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