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