Breaking out some logic and fixing some icons
[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.6"
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                                 self.refresh_session()
333                                 serviceId = self._defaultBackendId
334                                 loggedIn = True
335
336                         if not loggedIn:
337                                 with gtk_toolbox.gtk_lock():
338                                         loggedIn, serviceId = self._login_by_user(numOfAttempts)
339
340                         with gtk_toolbox.gtk_lock():
341                                 self._change_loggedin_status(serviceId)
342                 except StandardError, e:
343                         with gtk_toolbox.gtk_lock():
344                                 self._errorDisplay.push_exception(e)
345
346         def refresh_session(self):
347                 assert self._initDone, "Attempting login before app is fully loaded"
348                 if not self._deviceIsOnline:
349                         raise RuntimeError("Unable to login, device is not online")
350
351                 loggedIn = False
352                 if not loggedIn:
353                         loggedIn = self._login_by_cookie()
354                 if not loggedIn:
355                         loggedIn = self._login_by_settings()
356
357                 if not loggedIn:
358                         raise RuntimeError("Login Failed")
359
360         def _login_by_cookie(self):
361                 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
362                 if loggedIn:
363                         warnings.warn(
364                                 "Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId],
365                                 UserWarning, 2
366                         )
367                 return loggedIn
368
369         def _login_by_settings(self):
370                 username, password = self._credentials
371                 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
372                 if loggedIn:
373                         self._credentials = username, password
374                         warnings.warn(
375                                 "Logged into %r through settings" % self._phoneBackends[self._defaultBackendId],
376                                 UserWarning, 2
377                         )
378                 return loggedIn
379
380         def _login_by_user(self, numOfAttempts):
381                 loggedIn, (username, password) = False, self._credentials
382                 tmpServiceId = self.NULL_BACKEND
383                 for attemptCount in xrange(numOfAttempts):
384                         if loggedIn:
385                                 break
386                         availableServices = {
387                                 self.GV_BACKEND: "Google Voice",
388                                 self.GC_BACKEND: "Grand Central",
389                         }
390                         credentials = self._credentialsDialog.request_credentials_from(
391                                 availableServices, defaultCredentials = self._credentials
392                         )
393                         tmpServiceId, username, password = credentials
394                         loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
395
396                 if loggedIn:
397                         serviceId = tmpServiceId
398                         self._credentials = username, password
399                         warnings.warn(
400                                 "Logged into %r through user request" % self._phoneBackends[serviceId],
401                                 UserWarning, 2
402                         )
403                 else:
404                         serviceId = self.NULL_BACKEND
405
406                 return loggedIn, serviceId
407
408         def _select_action(self, action, number, message):
409                 self.refresh_session()
410                 if action == "select":
411                         self._dialpads[self._selectedBackendId].set_number(number)
412                         self._notebook.set_current_page(self.KEYPAD_TAB)
413                 elif action == "dial":
414                         self._on_dial_clicked(number)
415                 elif action == "sms":
416                         self._on_sms_clicked(number, message)
417                 else:
418                         assert False, "Unknown action: %s" % action
419
420         def _change_loggedin_status(self, newStatus):
421                 oldStatus = self._selectedBackendId
422                 if oldStatus == newStatus:
423                         return
424
425                 self._dialpads[oldStatus].disable()
426                 self._accountViews[oldStatus].disable()
427                 self._recentViews[oldStatus].disable()
428                 self._messagesViews[oldStatus].disable()
429                 self._contactsViews[oldStatus].disable()
430
431                 self._dialpads[newStatus].enable()
432                 self._accountViews[newStatus].enable()
433                 self._recentViews[newStatus].enable()
434                 self._messagesViews[newStatus].enable()
435                 self._contactsViews[newStatus].enable()
436
437                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
438                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
439                 self._accountViews[self._selectedBackendId].update()
440
441                 self._selectedBackendId = newStatus
442
443         def load_settings(self, config):
444                 """
445                 @note UI Thread
446                 """
447                 try:
448                         self._defaultBackendId = int(config.get(self.__pretty_app_name__, "active"))
449                         blobs = (
450                                 config.get(self.__pretty_app_name__, "bin_blob_%i" % i)
451                                 for i in xrange(len(self._credentials))
452                         )
453                         creds = (
454                                 base64.b64decode(blob)
455                                 for blob in blobs
456                         )
457                         self._credentials = tuple(creds)
458                 except ConfigParser.NoSectionError, e:
459                         warnings.warn(
460                                 "Settings file %s is missing section %s" % (
461                                         self._user_settings,
462                                         e.section,
463                                 ),
464                                 stacklevel=2
465                         )
466
467                 for backendId, view in itertools.chain(
468                         self._dialpads.iteritems(),
469                         self._accountViews.iteritems(),
470                         self._messagesViews.iteritems(),
471                         self._recentViews.iteritems(),
472                         self._contactsViews.iteritems(),
473                 ):
474                         sectionName = "%s - %s" % (backendId, view.name())
475                         try:
476                                 view.load_settings(config, sectionName)
477                         except ConfigParser.NoSectionError, e:
478                                 warnings.warn(
479                                         "Settings file %s is missing section %s" % (
480                                                 self._user_settings,
481                                                 e.section,
482                                         ),
483                                         stacklevel=2
484                                 )
485
486         def save_settings(self, config):
487                 """
488                 @note Thread Agnostic
489                 """
490                 config.add_section(self.__pretty_app_name__)
491                 config.set(self.__pretty_app_name__, "active", str(self._selectedBackendId))
492                 for i, value in enumerate(self._credentials):
493                         blob = base64.b64encode(value)
494                         config.set(self.__pretty_app_name__, "bin_blob_%i" % i, blob)
495                 for backendId, view in itertools.chain(
496                         self._dialpads.iteritems(),
497                         self._accountViews.iteritems(),
498                         self._messagesViews.iteritems(),
499                         self._recentViews.iteritems(),
500                         self._contactsViews.iteritems(),
501                 ):
502                         sectionName = "%s - %s" % (backendId, view.name())
503                         config.add_section(sectionName)
504                         view.save_settings(config, sectionName)
505
506         def _guess_preferred_backend(self, backendAndCookiePaths):
507                 modTimeAndPath = [
508                         (getmtime_nothrow(path), backendId, path)
509                         for backendId, path in backendAndCookiePaths
510                 ]
511                 modTimeAndPath.sort()
512                 return modTimeAndPath[-1][1]
513
514         def _save_settings(self):
515                 """
516                 @note Thread Agnostic
517                 """
518                 config = ConfigParser.SafeConfigParser()
519                 self.save_settings(config)
520                 with open(self._user_settings, "wb") as configFile:
521                         config.write(configFile)
522
523         def _on_close(self, *args, **kwds):
524                 try:
525                         if self._osso is not None:
526                                 self._osso.close()
527
528                         if self._initDone:
529                                 self._save_settings()
530                 finally:
531                         gtk.main_quit()
532
533         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
534                 """
535                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
536                 For system_inactivity, we have no background tasks to pause
537
538                 @note Hildon specific
539                 """
540                 if memory_low:
541                         for backendId in self.BACKENDS:
542                                 self._phoneBackends[backendId].clear_caches()
543                         self._contactsViews[self._selectedBackendId].clear_caches()
544                         gc.collect()
545
546                 if save_unsaved_data or shutdown:
547                         self._save_settings()
548
549         def _on_connection_change(self, connection, event, magicIdentifier):
550                 """
551                 @note Hildon specific
552                 """
553                 import conic
554
555                 status = event.get_status()
556                 error = event.get_error()
557                 iap_id = event.get_iap_id()
558                 bearer = event.get_bearer_type()
559
560                 if status == conic.STATUS_CONNECTED:
561                         self._deviceIsOnline = True
562                         if self._initDone:
563                                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2])
564                                 backgroundLogin.setDaemon(True)
565                                 backgroundLogin.start()
566                 elif status == conic.STATUS_DISCONNECTED:
567                         self._deviceIsOnline = False
568                         if self._initDone:
569                                 self._defaultBackendId = self._selectedBackendId
570                                 self._change_loggedin_status(self.NULL_BACKEND)
571
572         def _on_window_state_change(self, widget, event, *args):
573                 """
574                 @note Hildon specific
575                 """
576                 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
577                         self._isFullScreen = True
578                 else:
579                         self._isFullScreen = False
580
581         def _on_key_press(self, widget, event, *args):
582                 """
583                 @note Hildon specific
584                 """
585                 if event.keyval == gtk.keysyms.F6:
586                         if self._isFullScreen:
587                                 self._window.unfullscreen()
588                         else:
589                                 self._window.fullscreen()
590
591         def _on_clearcookies_clicked(self, *args):
592                 self._phoneBackends[self._selectedBackendId].logout()
593                 self._accountViews[self._selectedBackendId].clear()
594                 self._recentViews[self._selectedBackendId].clear()
595                 self._messagesViews[self._selectedBackendId].clear()
596                 self._contactsViews[self._selectedBackendId].clear()
597                 self._change_loggedin_status(self.NULL_BACKEND)
598
599                 backgroundLogin = threading.Thread(target=self.attempt_login, args=[2, True])
600                 backgroundLogin.setDaemon(True)
601                 backgroundLogin.start()
602
603         def _on_notebook_switch_page(self, notebook, page, page_num):
604                 if page_num == self.RECENT_TAB:
605                         self._recentViews[self._selectedBackendId].update()
606                 elif page_num == self.MESSAGES_TAB:
607                         self._messagesViews[self._selectedBackendId].update()
608                 elif page_num == self.CONTACTS_TAB:
609                         self._contactsViews[self._selectedBackendId].update()
610                 elif page_num == self.ACCOUNT_TAB:
611                         self._accountViews[self._selectedBackendId].update()
612
613                 tabTitle = self._notebook.get_tab_label(self._notebook.get_nth_page(page_num)).get_text()
614                 if hildon is not None:
615                         self._window.set_title(tabTitle)
616                 else:
617                         self._window.set_title("%s - %s" % (self.__pretty_app_name__, tabTitle))
618
619         def _on_sms_clicked(self, number, message):
620                 """
621                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
622                 """
623                 assert number
624                 assert message
625                 try:
626                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
627                 except RuntimeError, e:
628                         loggedIn = False
629                         self._errorDisplay.push_exception(e)
630                         return
631
632                 if not loggedIn:
633                         self._errorDisplay.push_message(
634                                 "Backend link with grandcentral is not working, please try again"
635                         )
636                         return
637
638                 dialed = False
639                 try:
640                         self._phoneBackends[self._selectedBackendId].send_sms(number, message)
641                         dialed = True
642                 except RuntimeError, e:
643                         self._errorDisplay.push_exception(e)
644                 except ValueError, e:
645                         self._errorDisplay.push_exception(e)
646
647         def _on_dial_clicked(self, number):
648                 """
649                 @todo Potential blocking on web access, maybe we should defer parts of this or put up a dialog?
650                 """
651                 assert number
652                 try:
653                         loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
654                 except RuntimeError, e:
655                         loggedIn = False
656                         self._errorDisplay.push_exception(e)
657                         return
658
659                 if not loggedIn:
660                         self._errorDisplay.push_message(
661                                 "Backend link with grandcentral is not working, please try again"
662                         )
663                         return
664
665                 dialed = False
666                 try:
667                         assert self._phoneBackends[self._selectedBackendId].get_callback_number() != ""
668                         self._phoneBackends[self._selectedBackendId].dial(number)
669                         dialed = True
670                 except RuntimeError, e:
671                         self._errorDisplay.push_exception(e)
672                 except ValueError, e:
673                         self._errorDisplay.push_exception(e)
674
675                 if dialed:
676                         self._dialpads[self._selectedBackendId].clear()
677
678         def _on_refresh(self, *args):
679                 page_num = self._notebook.get_current_page()
680                 if page_num == self.CONTACTS_TAB:
681                         self._contactsViews[self._selectedBackendId].update(force=True)
682                 elif page_num == self.RECENT_TAB:
683                         self._recentViews[self._selectedBackendId].update(force=True)
684                 elif page_num == self.MESSAGES_TAB:
685                         self._messagesViews[self._selectedBackendId].update(force=True)
686
687         def _on_paste(self, *args):
688                 contents = self._clipboard.wait_for_text()
689                 self._dialpads[self._selectedBackendId].set_number(contents)
690
691         def _on_about_activate(self, *args):
692                 dlg = gtk.AboutDialog()
693                 dlg.set_name(self.__pretty_app_name__)
694                 dlg.set_version(self.__version__)
695                 dlg.set_copyright("Copyright 2008 - LGPL")
696                 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")
697                 dlg.set_website("http://gc-dialer.garage.maemo.org/")
698                 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
699                 dlg.run()
700                 dlg.destroy()
701
702
703 def run_doctest():
704         import doctest
705
706         failureCount, testCount = doctest.testmod()
707         if not failureCount:
708                 print "Tests Successful"
709                 sys.exit(0)
710         else:
711                 sys.exit(1)
712
713
714 def run_dialpad():
715         gtk.gdk.threads_init()
716         if hildon is not None:
717                 gtk.set_application_name(Dialcentral.__pretty_app_name__)
718         handle = Dialcentral()
719         gtk.main()
720
721
722 class DummyOptions(object):
723
724         def __init__(self):
725                 self.test = False
726
727
728 if __name__ == "__main__":
729         if len(sys.argv) > 1:
730                 try:
731                         import optparse
732                 except ImportError:
733                         optparse = None
734
735                 if optparse is not None:
736                         parser = optparse.OptionParser()
737                         parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
738                         (commandOptions, commandArgs) = parser.parse_args()
739         else:
740                 commandOptions = DummyOptions()
741                 commandArgs = []
742
743         if commandOptions.test:
744                 run_doctest()
745         else:
746                 run_dialpad()