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