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