Improving login speed for new users
[gc-dialer] / src / dc_glade.py
1 #!/usr/bin/python2.5
2
3 """
4 DialCentral - Front end for Google's GoogleVoice 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 _moduleLogger = logging.getLogger("dc_glade")
43 PROFILE_STARTUP = False
44
45
46 def getmtime_nothrow(path):
47         try:
48                 return os.path.getmtime(path)
49         except Exception:
50                 return 0
51
52
53 def display_error_message(msg):
54         error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
55
56         def close(dialog, response):
57                 dialog.destroy()
58         error_dialog.connect("response", close)
59         error_dialog.run()
60
61
62 class Dialcentral(object):
63
64         _glade_files = [
65                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
66                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
67                 '/usr/lib/dialcentral/dialcentral.glade',
68         ]
69
70         KEYPAD_TAB = 0
71         RECENT_TAB = 1
72         MESSAGES_TAB = 2
73         CONTACTS_TAB = 3
74         ACCOUNT_TAB = 4
75
76         NULL_BACKEND = 0
77         # 1 Was GrandCentral support so the gap was maintained for compatibility
78         GV_BACKEND = 2
79         BACKENDS = (NULL_BACKEND, GV_BACKEND)
80
81         def __init__(self):
82                 self._initDone = False
83                 self._connection = None
84                 self._osso = None
85                 self._clipboard = gtk.clipboard_get()
86
87                 self._credentials = ("", "")
88                 self._selectedBackendId = self.NULL_BACKEND
89                 self._defaultBackendId = self.GV_BACKEND
90                 self._phoneBackends = None
91                 self._dialpads = None
92                 self._accountViews = None
93                 self._messagesViews = None
94                 self._recentViews = None
95                 self._contactsViews = None
96                 self._alarmHandler = None
97                 self._ledHandler = None
98                 self._originalCurrentLabels = []
99
100                 for path in self._glade_files:
101                         if os.path.isfile(path):
102                                 self._widgetTree = gtk.glade.XML(path)
103                                 break
104                 else:
105                         display_error_message("Cannot find dialcentral.glade")
106                         gtk.main_quit()
107                         return
108
109                 self._window = self._widgetTree.get_widget("mainWindow")
110                 self._notebook = self._widgetTree.get_widget("notebook")
111                 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
112                 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
113
114                 self._isFullScreen = False
115                 self._app = hildonize.get_app_class()()
116                 self._window = hildonize.hildonize_window(self._app, self._window)
117                 hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
118                 hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
119
120                 for scrollingWidgetName in (
121                         'recent_scrolledwindow',
122                         'message_scrolledwindow',
123                         'contacts_scrolledwindow',
124                         "smsMessages_scrolledwindow",
125                 ):
126                         scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
127                         assert scrollingWidget is not None, scrollingWidgetName
128                         hildonize.hildonize_scrollwindow(scrollingWidget)
129                 for scrollingWidgetName in (
130                         "smsMessage_scrolledEntry",
131                 ):
132                         scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
133                         assert scrollingWidget is not None, scrollingWidgetName
134                         hildonize.hildonize_scrollwindow_with_viewport(scrollingWidget)
135
136                 for buttonName in (
137                         "back",
138                         "addressbookSelectButton",
139                         "sendSmsButton",
140                         "dialButton",
141                         "cancelSmsButton",
142                         "callbackSelectButton",
143                         "minutesEntryButton",
144                         "clearcookies",
145                         "phoneTypeSelection",
146                 ):
147                         button = self._widgetTree.get_widget(buttonName)
148                         assert button is not None, buttonName
149                         hildonize.set_button_thumb_selectable(button)
150
151                 replacementButtons = [gtk.Button("Test")]
152                 menu = hildonize.hildonize_menu(
153                         self._window,
154                         self._widgetTree.get_widget("dialpad_menubar"),
155                         replacementButtons
156                 )
157
158                 self._window.connect("key-press-event", self._on_key_press)
159                 self._window.connect("window-state-event", self._on_window_state_change)
160                 if not hildonize.IS_HILDON_SUPPORTED:
161                         _moduleLogger.warning("No hildonization support")
162
163                 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
164
165                 self._window.connect("destroy", self._on_close)
166                 self._window.set_default_size(800, 300)
167                 self._window.show_all()
168
169                 self._loginSink = gtk_toolbox.threaded_stage(
170                         gtk_toolbox.comap(
171                                 self._attempt_login,
172                                 gtk_toolbox.null_sink(),
173                         )
174                 )
175
176                 if not PROFILE_STARTUP:
177                         backgroundSetup = threading.Thread(target=self._idle_setup)
178                         backgroundSetup.setDaemon(True)
179                         backgroundSetup.start()
180                 else:
181                         self._idle_setup()
182
183         def _idle_setup(self):
184                 """
185                 If something can be done after the UI loads, push it here so it's not blocking the UI
186                 """
187                 # Barebones UI handlers
188                 try:
189                         import null_backend
190                         import null_views
191
192                         self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
193                         with gtk_toolbox.gtk_lock():
194                                 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
195                                 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
196                                 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
197                                 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
198                                 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
199
200                                 self._dialpads[self._selectedBackendId].enable()
201                                 self._accountViews[self._selectedBackendId].enable()
202                                 self._recentViews[self._selectedBackendId].enable()
203                                 self._messagesViews[self._selectedBackendId].enable()
204                                 self._contactsViews[self._selectedBackendId].enable()
205                 except Exception, e:
206                         with gtk_toolbox.gtk_lock():
207                                 self._errorDisplay.push_exception()
208
209                 # Setup maemo specifics
210                 try:
211                         try:
212                                 import osso
213                         except (ImportError, OSError):
214                                 osso = None
215                         self._osso = None
216                         if osso is not None:
217                                 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
218                                 device = osso.DeviceState(self._osso)
219                                 device.set_device_state_callback(self._on_device_state_change, 0)
220                         else:
221                                 _moduleLogger.warning("No device state support")
222
223                         try:
224                                 import alarm_handler
225                                 if alarm_handler.AlarmHandler is not alarm_handler._NoneAlarmHandler:
226                                         self._alarmHandler = alarm_handler.AlarmHandler()
227                                 else:
228                                         self._alarmHandler = None
229                         except (ImportError, OSError):
230                                 alarm_handler = None
231                         except Exception:
232                                 with gtk_toolbox.gtk_lock():
233                                         self._errorDisplay.push_exception()
234                                 alarm_handler = None
235                         if alarm_handler is None:
236                                 _moduleLogger.warning("No notification support")
237                         if hildonize.IS_HILDON_SUPPORTED:
238                                 try:
239                                         import led_handler
240                                         self._ledHandler = led_handler.LedHandler()
241                                 except Exception, e:
242                                         _moduleLogger.exception('LED Handling failed: "%s"' % str(e))
243                                         self._ledHandler = None
244                         else:
245                                 self._ledHandler = None
246
247                         try:
248                                 import conic
249                         except (ImportError, OSError):
250                                 conic = None
251                         self._connection = None
252                         if conic is not None:
253                                 self._connection = conic.Connection()
254                                 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
255                                 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
256                         else:
257                                 _moduleLogger.warning("No connection support")
258                 except Exception, e:
259                         with gtk_toolbox.gtk_lock():
260                                 self._errorDisplay.push_exception()
261
262                 # Setup costly backends
263                 try:
264                         import gv_backend
265                         import file_backend
266                         import gv_views
267
268                         try:
269                                 os.makedirs(constants._data_path_)
270                         except OSError, e:
271                                 if e.errno != 17:
272                                         raise
273                         gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
274
275                         self._phoneBackends.update({
276                                 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
277                         })
278                         with gtk_toolbox.gtk_lock():
279                                 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
280                                 self._dialpads.update({
281                                         self.GV_BACKEND: unifiedDialpad,
282                                 })
283                                 self._accountViews.update({
284                                         self.GV_BACKEND: gv_views.AccountInfo(
285                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
286                                         ),
287                                 })
288                                 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
289                                 self._recentViews.update({
290                                         self.GV_BACKEND: gv_views.RecentCallsView(
291                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
292                                         ),
293                                 })
294                                 self._messagesViews.update({
295                                         self.GV_BACKEND: gv_views.MessagesView(
296                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
297                                         ),
298                                 })
299                                 self._contactsViews.update({
300                                         self.GV_BACKEND: gv_views.ContactsView(
301                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
302                                         ),
303                                 })
304
305                         fsContactsPath = os.path.join(constants._data_path_, "contacts")
306                         fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
307
308                         self._dialpads[self.GV_BACKEND].number_selected = self._select_action
309                         self._recentViews[self.GV_BACKEND].number_selected = self._select_action
310                         self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
311                         self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
312
313                         addressBooks = [
314                                 self._phoneBackends[self.GV_BACKEND],
315                                 fileBackend,
316                         ]
317                         mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
318                         self._contactsViews[self.GV_BACKEND].append(mergedBook)
319                         self._contactsViews[self.GV_BACKEND].extend(addressBooks)
320                         self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
321
322                         callbackMapping = {
323                                 "on_paste": self._on_paste,
324                                 "on_refresh": self._on_menu_refresh,
325                                 "on_clearcookies_clicked": self._on_clearcookies_clicked,
326                                 "on_about_activate": self._on_about_activate,
327                         }
328                         if hildonize.GTK_MENU_USED:
329                                 self._widgetTree.signal_autoconnect(callbackMapping)
330                         self._notebook.connect("switch-page", self._on_notebook_switch_page)
331                         self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked)
332
333                         with gtk_toolbox.gtk_lock():
334                                 self._originalCurrentLabels = [
335                                         self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
336                                         for pageIndex in xrange(self._notebook.get_n_pages())
337                                 ]
338                                 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
339                                 self._notebookTapHandler.enable()
340                         self._notebookTapHandler.on_tap = self._reset_tab_refresh
341                         self._notebookTapHandler.on_hold = self._on_tab_refresh
342                         self._notebookTapHandler.on_holding = self._set_tab_refresh
343                         self._notebookTapHandler.on_cancel = self._reset_tab_refresh
344
345                         self._initDone = True
346
347                         config = ConfigParser.SafeConfigParser()
348                         config.read(constants._user_settings_)
349                         with gtk_toolbox.gtk_lock():
350                                 self.load_settings(config)
351                 except Exception, e:
352                         with gtk_toolbox.gtk_lock():
353                                 self._errorDisplay.push_exception()
354                 finally:
355                         self._spawn_attempt_login()
356
357         def _spawn_attempt_login(self, *args):
358                 self._loginSink.send(args)
359
360         def _attempt_login(self, force = False):
361                 """
362                 @note This must be run outside of the UI lock
363                 """
364                 try:
365                         assert self._initDone, "Attempting login before app is fully loaded"
366
367                         serviceId = self.NULL_BACKEND
368                         loggedIn = False
369                         if not force and self._defaultBackendId != self.NULL_BACKEND:
370                                 with gtk_toolbox.gtk_lock():
371                                         banner = hildonize.show_busy_banner_start(self._window, "Logging In...")
372                                 try:
373                                         self.refresh_session()
374                                         serviceId = self._defaultBackendId
375                                         loggedIn = True
376                                 except Exception, e:
377                                         _moduleLogger.exception('Session refresh failed with the following message "%s"' % str(e))
378                                 finally:
379                                         with gtk_toolbox.gtk_lock():
380                                                 hildonize.show_busy_banner_end(banner)
381
382                         if not loggedIn:
383                                 loggedIn, serviceId = self._login_by_user()
384
385                         with gtk_toolbox.gtk_lock():
386                                 self._change_loggedin_status(serviceId)
387                                 if loggedIn:
388                                         hildonize.show_information_banner(self._window, "Logged In")
389                                 else:
390                                         hildonize.show_information_banner(self._window, "Login Failed")
391                                 if not self._phoneBackends[self._defaultBackendId].get_callback_number():
392                                         # subtle reminder to the users to configure things
393                                         self._notebook.set_current_page(self.ACCOUNT_TAB)
394
395                 except Exception, e:
396                         with gtk_toolbox.gtk_lock():
397                                 self._errorDisplay.push_exception()
398
399         def refresh_session(self):
400                 """
401                 @note Thread agnostic
402                 """
403                 assert self._initDone, "Attempting login before app is fully loaded"
404
405                 loggedIn = False
406                 if not loggedIn:
407                         loggedIn = self._login_by_cookie()
408                 if not loggedIn:
409                         loggedIn = self._login_by_settings()
410
411                 if not loggedIn:
412                         raise RuntimeError("Login Failed")
413
414         def _login_by_cookie(self):
415                 """
416                 @note Thread agnostic
417                 """
418                 if self._credentials == ("", ""):
419                         # Disallow logging in by cookie alone, without credentials
420                         return False
421
422                 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
423                 if loggedIn:
424                         _moduleLogger.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
425                 return loggedIn
426
427         def _login_by_settings(self):
428                 """
429                 @note Thread agnostic
430                 """
431                 if self._credentials == ("", ""):
432                         # Don't bother with the settings if they are blank
433                         return False
434
435                 username, password = self._credentials
436                 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
437                 if loggedIn:
438                         self._credentials = username, password
439                         _moduleLogger.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
440                 return loggedIn
441
442         def _login_by_user(self):
443                 """
444                 @note This must be run outside of the UI lock
445                 """
446                 loggedIn, (username, password) = False, self._credentials
447                 tmpServiceId = self.GV_BACKEND
448                 while not loggedIn:
449                         with gtk_toolbox.gtk_lock():
450                                 credentials = self._credentialsDialog.request_credentials(
451                                         defaultCredentials = self._credentials
452                                 )
453                                 banner = hildonize.show_busy_banner_start(self._window, "Logging In...")
454                         try:
455                                 username, password = credentials
456                                 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
457                         finally:
458                                 with gtk_toolbox.gtk_lock():
459                                         hildonize.show_busy_banner_end(banner)
460
461                 if loggedIn:
462                         serviceId = tmpServiceId
463                         self._credentials = username, password
464                         _moduleLogger.info("Logged into %r through user request" % self._phoneBackends[serviceId])
465                 else:
466                         # Hint to the user that they are not logged in
467                         serviceId = self.NULL_BACKEND
468                         self._notebook.set_current_page(self.ACCOUNT_TAB)
469
470                 return loggedIn, serviceId
471
472         def _select_action(self, action, number, message):
473                 self.refresh_session()
474                 if action == "dial":
475                         self._on_dial_clicked(number)
476                 elif action == "sms":
477                         self._on_sms_clicked(number, message)
478                 else:
479                         assert False, "Unknown action: %s" % action
480
481         def _change_loggedin_status(self, newStatus):
482                 oldStatus = self._selectedBackendId
483                 if oldStatus == newStatus:
484                         return
485
486                 self._dialpads[oldStatus].disable()
487                 self._accountViews[oldStatus].disable()
488                 self._recentViews[oldStatus].disable()
489                 self._messagesViews[oldStatus].disable()
490                 self._contactsViews[oldStatus].disable()
491
492                 self._dialpads[newStatus].enable()
493                 self._accountViews[newStatus].enable()
494                 self._recentViews[newStatus].enable()
495                 self._messagesViews[newStatus].enable()
496                 self._contactsViews[newStatus].enable()
497
498                 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
499                         self._phoneBackends[self._selectedBackendId].set_sane_callback()
500
501                 self._selectedBackendId = newStatus
502
503                 self._accountViews[self._selectedBackendId].update()
504                 self._refresh_active_tab()
505
506         def load_settings(self, config):
507                 """
508                 @note UI Thread
509                 """
510                 try:
511                         if not PROFILE_STARTUP:
512                                 self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
513                         else:
514                                 self._defaultBackendId = self.NULL_BACKEND
515                         blobs = (
516                                 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
517                                 for i in xrange(len(self._credentials))
518                         )
519                         creds = (
520                                 base64.b64decode(blob)
521                                 for blob in blobs
522                         )
523                         self._credentials = tuple(creds)
524
525                         if self._alarmHandler is not None:
526                                 self._alarmHandler.load_settings(config, "alarm")
527                 except ConfigParser.NoOptionError, e:
528                         _moduleLogger.exception(
529                                 "Settings file %s is missing section %s" % (
530                                         constants._user_settings_,
531                                         e.section,
532                                 ),
533                         )
534                 except ConfigParser.NoSectionError, e:
535                         _moduleLogger.exception(
536                                 "Settings file %s is missing section %s" % (
537                                         constants._user_settings_,
538                                         e.section,
539                                 ),
540                         )
541
542                 for backendId, view in itertools.chain(
543                         self._dialpads.iteritems(),
544                         self._accountViews.iteritems(),
545                         self._messagesViews.iteritems(),
546                         self._recentViews.iteritems(),
547                         self._contactsViews.iteritems(),
548                 ):
549                         sectionName = "%s - %s" % (backendId, view.name())
550                         try:
551                                 view.load_settings(config, sectionName)
552                         except ConfigParser.NoOptionError, e:
553                                 _moduleLogger.exception(
554                                         "Settings file %s is missing section %s" % (
555                                                 constants._user_settings_,
556                                                 e.section,
557                                         ),
558                                 )
559                         except ConfigParser.NoSectionError, e:
560                                 _moduleLogger.exception(
561                                         "Settings file %s is missing section %s" % (
562                                                 constants._user_settings_,
563                                                 e.section,
564                                         ),
565                                 )
566
567                 try:
568                         previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
569                         if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
570                                 hildonize.window_to_landscape(self._window)
571                         elif previousOrientation == gtk.ORIENTATION_VERTICAL:
572                                 hildonize.window_to_portrait(self._window)
573                 except ConfigParser.NoOptionError, e:
574                         _moduleLogger.exception(
575                                 "Settings file %s is missing section %s" % (
576                                         constants._user_settings_,
577                                         e.section,
578                                 ),
579                         )
580                 except ConfigParser.NoSectionError, e:
581                         _moduleLogger.exception(
582                                 "Settings file %s is missing section %s" % (
583                                         constants._user_settings_,
584                                         e.section,
585                                 ),
586                         )
587
588         def save_settings(self, config):
589                 """
590                 @note Thread Agnostic
591                 """
592                 # Because we now only support GVoice, if there are user credentials,
593                 # always assume its using the GVoice backend
594                 if self._credentials[0] and self._credentials[1]:
595                         backend = self.GV_BACKEND
596                 else:
597                         backend = self.NULL_BACKEND
598
599                 config.add_section(constants.__pretty_app_name__)
600                 config.set(constants.__pretty_app_name__, "active", str(backend))
601                 config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
602                 for i, value in enumerate(self._credentials):
603                         blob = base64.b64encode(value)
604                         config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
605                 config.add_section("alarm")
606                 if self._alarmHandler is not None:
607                         self._alarmHandler.save_settings(config, "alarm")
608
609                 for backendId, view in itertools.chain(
610                         self._dialpads.iteritems(),
611                         self._accountViews.iteritems(),
612                         self._messagesViews.iteritems(),
613                         self._recentViews.iteritems(),
614                         self._contactsViews.iteritems(),
615                 ):
616                         sectionName = "%s - %s" % (backendId, view.name())
617                         config.add_section(sectionName)
618                         view.save_settings(config, sectionName)
619
620         def _save_settings(self):
621                 """
622                 @note Thread Agnostic
623                 """
624                 config = ConfigParser.SafeConfigParser()
625                 self.save_settings(config)
626                 with open(constants._user_settings_, "wb") as configFile:
627                         config.write(configFile)
628
629         def _refresh_active_tab(self):
630                 pageIndex = self._notebook.get_current_page()
631                 if pageIndex == self.CONTACTS_TAB:
632                         self._contactsViews[self._selectedBackendId].update(force=True)
633                 elif pageIndex == self.RECENT_TAB:
634                         self._recentViews[self._selectedBackendId].update(force=True)
635                 elif pageIndex == self.MESSAGES_TAB:
636                         self._messagesViews[self._selectedBackendId].update(force=True)
637
638                 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
639                         if self._ledHandler is not None:
640                                 self._ledHandler.off()
641
642         def _on_close(self, *args, **kwds):
643                 try:
644                         if self._osso is not None:
645                                 self._osso.close()
646
647                         if self._initDone:
648                                 self._save_settings()
649                 finally:
650                         gtk.main_quit()
651
652         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
653                 """
654                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
655                 For system_inactivity, we have no background tasks to pause
656
657                 @note Hildon specific
658                 """
659                 try:
660                         if memory_low:
661                                 for backendId in self.BACKENDS:
662                                         self._phoneBackends[backendId].clear_caches()
663                                 self._contactsViews[self._selectedBackendId].clear_caches()
664                                 gc.collect()
665
666                         if save_unsaved_data or shutdown:
667                                 self._save_settings()
668                 except Exception, e:
669                         self._errorDisplay.push_exception()
670
671         def _on_connection_change(self, connection, event, magicIdentifier):
672                 """
673                 @note Hildon specific
674                 """
675                 try:
676                         import conic
677
678                         status = event.get_status()
679                         error = event.get_error()
680                         iap_id = event.get_iap_id()
681                         bearer = event.get_bearer_type()
682
683                         if status == conic.STATUS_CONNECTED:
684                                 if self._initDone:
685                                         self._spawn_attempt_login()
686                         elif status == conic.STATUS_DISCONNECTED:
687                                 if self._initDone:
688                                         self._defaultBackendId = self._selectedBackendId
689                                         self._change_loggedin_status(self.NULL_BACKEND)
690                 except Exception, e:
691                         self._errorDisplay.push_exception()
692
693         def _on_window_state_change(self, widget, event, *args):
694                 """
695                 @note Hildon specific
696                 """
697                 try:
698                         if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
699                                 self._isFullScreen = True
700                         else:
701                                 self._isFullScreen = False
702                 except Exception, e:
703                         self._errorDisplay.push_exception()
704
705         def _on_key_press(self, widget, event, *args):
706                 """
707                 @note Hildon specific
708                 """
709                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
710                 try:
711                         if (
712                                 event.keyval == gtk.keysyms.F6 or
713                                 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
714                         ):
715                                 if self._isFullScreen:
716                                         self._window.unfullscreen()
717                                 else:
718                                         self._window.fullscreen()
719                 except Exception, e:
720                         self._errorDisplay.push_exception()
721
722         def _on_clearcookies_clicked(self, *args):
723                 try:
724                         self._phoneBackends[self._selectedBackendId].logout()
725                         self._accountViews[self._selectedBackendId].clear()
726                         self._recentViews[self._selectedBackendId].clear()
727                         self._messagesViews[self._selectedBackendId].clear()
728                         self._contactsViews[self._selectedBackendId].clear()
729                         self._change_loggedin_status(self.NULL_BACKEND)
730
731                         self._spawn_attempt_login(True)
732                 except Exception, e:
733                         self._errorDisplay.push_exception()
734
735         def _on_notebook_switch_page(self, notebook, page, pageIndex):
736                 try:
737                         self._reset_tab_refresh()
738
739                         didRecentUpdate = False
740                         didMessagesUpdate = False
741
742                         if pageIndex == self.RECENT_TAB:
743                                 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
744                         elif pageIndex == self.MESSAGES_TAB:
745                                 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
746                         elif pageIndex == self.CONTACTS_TAB:
747                                 self._contactsViews[self._selectedBackendId].update()
748                         elif pageIndex == self.ACCOUNT_TAB:
749                                 self._accountViews[self._selectedBackendId].update()
750
751                         if didRecentUpdate or didMessagesUpdate:
752                                 if self._ledHandler is not None:
753                                         self._ledHandler.off()
754                 except Exception, e:
755                         self._errorDisplay.push_exception()
756
757         def _set_tab_refresh(self, *args):
758                 try:
759                         pageIndex = self._notebook.get_current_page()
760                         child = self._notebook.get_nth_page(pageIndex)
761                         self._notebook.get_tab_label(child).set_text("Refresh?")
762                 except Exception, e:
763                         self._errorDisplay.push_exception()
764                 return False
765
766         def _reset_tab_refresh(self, *args):
767                 try:
768                         pageIndex = self._notebook.get_current_page()
769                         child = self._notebook.get_nth_page(pageIndex)
770                         self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
771                 except Exception, e:
772                         self._errorDisplay.push_exception()
773                 return False
774
775         def _on_tab_refresh(self, *args):
776                 try:
777                         self._refresh_active_tab()
778                         self._reset_tab_refresh()
779                 except Exception, e:
780                         self._errorDisplay.push_exception()
781                 return False
782
783         def _on_sms_clicked(self, number, message):
784                 try:
785                         assert number, "No number specified"
786                         assert message, "Empty message"
787                         try:
788                                 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
789                         except Exception, e:
790                                 loggedIn = False
791                                 self._errorDisplay.push_exception()
792                                 return
793
794                         if not loggedIn:
795                                 self._errorDisplay.push_message(
796                                         "Backend link with GoogleVoice is not working, please try again"
797                                 )
798                                 return
799
800                         dialed = False
801                         try:
802                                 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
803                                 hildonize.show_information_banner(self._window, "Sending to %s" % number)
804                                 dialed = True
805                         except Exception, e:
806                                 self._errorDisplay.push_exception()
807
808                         if dialed:
809                                 self._dialpads[self._selectedBackendId].clear()
810                 except Exception, e:
811                         self._errorDisplay.push_exception()
812
813         def _on_dial_clicked(self, number):
814                 try:
815                         assert number, "No number to call"
816                         try:
817                                 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
818                         except Exception, e:
819                                 loggedIn = False
820                                 self._errorDisplay.push_exception()
821                                 return
822
823                         if not loggedIn:
824                                 self._errorDisplay.push_message(
825                                         "Backend link with GoogleVoice is not working, please try again"
826                                 )
827                                 return
828
829                         dialed = False
830                         try:
831                                 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
832                                 self._phoneBackends[self._selectedBackendId].dial(number)
833                                 hildonize.show_information_banner(self._window, "Calling %s" % number)
834                                 dialed = True
835                         except Exception, e:
836                                 self._errorDisplay.push_exception()
837
838                         if dialed:
839                                 self._dialpads[self._selectedBackendId].clear()
840                 except Exception, e:
841                         self._errorDisplay.push_exception()
842
843         def _on_menu_refresh(self, *args):
844                 try:
845                         self._refresh_active_tab()
846                 except Exception, e:
847                         self._errorDisplay.push_exception()
848
849         def _on_paste(self, *args):
850                 try:
851                         contents = self._clipboard.wait_for_text()
852                         if contents is not None:
853                                 self._dialpads[self._selectedBackendId].set_number(contents)
854                 except Exception, e:
855                         self._errorDisplay.push_exception()
856
857         def _on_about_activate(self, *args):
858                 try:
859                         dlg = gtk.AboutDialog()
860                         dlg.set_name(constants.__pretty_app_name__)
861                         dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
862                         dlg.set_copyright("Copyright 2008 - LGPL")
863                         dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account.  This application is not affiliated with Google in any way")
864                         dlg.set_website("http://gc-dialer.garage.maemo.org/")
865                         dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
866                         dlg.run()
867                         dlg.destroy()
868                 except Exception, e:
869                         self._errorDisplay.push_exception()
870
871
872 def run_doctest():
873         import doctest
874
875         failureCount, testCount = doctest.testmod()
876         if not failureCount:
877                 print "Tests Successful"
878                 sys.exit(0)
879         else:
880                 sys.exit(1)
881
882
883 def run_dialpad():
884         _lock_file = os.path.join(constants._data_path_, ".lock")
885         #with gtk_toolbox.flock(_lock_file, 0):
886         gtk.gdk.threads_init()
887
888         if hildonize.IS_HILDON_SUPPORTED:
889                 gtk.set_application_name(constants.__pretty_app_name__)
890         handle = Dialcentral()
891         if not PROFILE_STARTUP:
892                 gtk.main()
893
894
895 class DummyOptions(object):
896
897         def __init__(self):
898                 self.test = False
899
900
901 if __name__ == "__main__":
902         logging.basicConfig(level=logging.DEBUG)
903         try:
904                 if len(sys.argv) > 1:
905                         try:
906                                 import optparse
907                         except ImportError:
908                                 optparse = None
909
910                         if optparse is not None:
911                                 parser = optparse.OptionParser()
912                                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
913                                 (commandOptions, commandArgs) = parser.parse_args()
914                 else:
915                         commandOptions = DummyOptions()
916                         commandArgs = []
917
918                 if commandOptions.test:
919                         run_doctest()
920                 else:
921                         run_dialpad()
922         finally:
923                 logging.shutdown()