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