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