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