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