More tweaks to improve login experience, attempting fresher credentials
[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                         config = ConfigParser.SafeConfigParser()
346                         config.read(constants._user_settings_)
347                         with gtk_toolbox.gtk_lock():
348                                 self.load_settings(config)
349                 except Exception, e:
350                         with gtk_toolbox.gtk_lock():
351                                 self._errorDisplay.push_exception()
352                 finally:
353                         self._initDone = True
354                         self._spawn_attempt_login()
355
356         def _spawn_attempt_login(self, *args):
357                 self._loginSink.send(args)
358
359         def _attempt_login(self, force = False):
360                 """
361                 @note This must be run outside of the UI lock
362                 """
363                 try:
364                         assert self._initDone, "Attempting login before app is fully loaded"
365
366                         serviceId = self.NULL_BACKEND
367                         loggedIn = False
368                         if not force and self._defaultBackendId != self.NULL_BACKEND:
369                                 with gtk_toolbox.gtk_lock():
370                                         banner = hildonize.show_busy_banner_start(self._window, "Logging In...")
371                                 try:
372                                         self.refresh_session()
373                                         serviceId = self._defaultBackendId
374                                         loggedIn = True
375                                 except Exception, e:
376                                         _moduleLogger.exception('Session refresh failed with the following message "%s"' % str(e))
377                                 finally:
378                                         with gtk_toolbox.gtk_lock():
379                                                 hildonize.show_busy_banner_end(banner)
380
381                         if not loggedIn:
382                                 loggedIn, serviceId = self._login_by_user()
383
384                         with gtk_toolbox.gtk_lock():
385                                 self._change_loggedin_status(serviceId)
386                                 if loggedIn:
387                                         hildonize.show_information_banner(self._window, "Logged In")
388                                 else:
389                                         hildonize.show_information_banner(self._window, "Login Failed")
390                                 if not self._phoneBackends[self._defaultBackendId].get_callback_number():
391                                         # subtle reminder to the users to configure things
392                                         self._notebook.set_current_page(self.ACCOUNT_TAB)
393
394                 except Exception, e:
395                         with gtk_toolbox.gtk_lock():
396                                 self._errorDisplay.push_exception()
397
398         def refresh_session(self):
399                 """
400                 @note Thread agnostic
401                 """
402                 assert self._initDone, "Attempting login before app is fully loaded"
403
404                 loggedIn = False
405                 if not loggedIn:
406                         loggedIn = self._login_by_cookie()
407                 if not loggedIn:
408                         loggedIn = self._login_by_settings()
409
410                 if not loggedIn:
411                         raise RuntimeError("Login Failed")
412
413         def _login_by_cookie(self):
414                 """
415                 @note Thread agnostic
416                 """
417                 loggedIn = False
418                 if self._credentials == ("", ""):
419                         # Disallow logging in by cookie alone, without credentials
420                         loggedIn = False
421
422                 if not loggedIn:
423                         loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
424
425                 if loggedIn:
426                         _moduleLogger.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
427                 else:
428                         # If the cookies are bad, scratch them completely
429                         self._phoneBackends[self._defaultBackendId].logout()
430
431                 return loggedIn
432
433         def _login_by_settings(self):
434                 """
435                 @note Thread agnostic
436                 """
437                 if self._credentials == ("", ""):
438                         # Don't bother with the settings if they are blank
439                         return False
440
441                 username, password = self._credentials
442                 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
443                 if loggedIn:
444                         self._credentials = username, password
445                         _moduleLogger.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
446                 return loggedIn
447
448         def _login_by_user(self):
449                 """
450                 @note This must be run outside of the UI lock
451                 """
452                 loggedIn, (username, password) = False, self._credentials
453                 tmpServiceId = self.GV_BACKEND
454                 while not loggedIn:
455                         with gtk_toolbox.gtk_lock():
456                                 credentials = self._credentialsDialog.request_credentials(
457                                         defaultCredentials = self._credentials
458                                 )
459                                 banner = hildonize.show_busy_banner_start(self._window, "Logging In...")
460                         try:
461                                 username, password = credentials
462                                 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
463                         finally:
464                                 with gtk_toolbox.gtk_lock():
465                                         hildonize.show_busy_banner_end(banner)
466
467                 if loggedIn:
468                         serviceId = tmpServiceId
469                         self._credentials = username, password
470                         _moduleLogger.info("Logged into %r through user request" % self._phoneBackends[serviceId])
471                 else:
472                         # Hint to the user that they are not logged in
473                         serviceId = self.NULL_BACKEND
474                         self._notebook.set_current_page(self.ACCOUNT_TAB)
475
476                 return loggedIn, serviceId
477
478         def _select_action(self, action, number, message):
479                 self.refresh_session()
480                 if action == "dial":
481                         self._on_dial_clicked(number)
482                 elif action == "sms":
483                         self._on_sms_clicked(number, message)
484                 else:
485                         assert False, "Unknown action: %s" % action
486
487         def _change_loggedin_status(self, newStatus):
488                 oldStatus = self._selectedBackendId
489                 if oldStatus == newStatus:
490                         return
491
492                 self._dialpads[oldStatus].disable()
493                 self._accountViews[oldStatus].disable()
494                 self._recentViews[oldStatus].disable()
495                 self._messagesViews[oldStatus].disable()
496                 self._contactsViews[oldStatus].disable()
497
498                 self._dialpads[newStatus].enable()
499                 self._accountViews[newStatus].enable()
500                 self._recentViews[newStatus].enable()
501                 self._messagesViews[newStatus].enable()
502                 self._contactsViews[newStatus].enable()
503
504                 self._selectedBackendId = newStatus
505
506                 self._accountViews[self._selectedBackendId].update()
507                 self._refresh_active_tab()
508
509         def load_settings(self, config):
510                 """
511                 @note UI Thread
512                 """
513                 try:
514                         if not PROFILE_STARTUP:
515                                 self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
516                         else:
517                                 self._defaultBackendId = self.NULL_BACKEND
518                         blobs = (
519                                 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
520                                 for i in xrange(len(self._credentials))
521                         )
522                         creds = (
523                                 base64.b64decode(blob)
524                                 for blob in blobs
525                         )
526                         self._credentials = tuple(creds)
527
528                         if self._alarmHandler is not None:
529                                 self._alarmHandler.load_settings(config, "alarm")
530                 except ConfigParser.NoOptionError, e:
531                         _moduleLogger.exception(
532                                 "Settings file %s is missing section %s" % (
533                                         constants._user_settings_,
534                                         e.section,
535                                 ),
536                         )
537                 except ConfigParser.NoSectionError, e:
538                         _moduleLogger.exception(
539                                 "Settings file %s is missing section %s" % (
540                                         constants._user_settings_,
541                                         e.section,
542                                 ),
543                         )
544
545                 for backendId, view in itertools.chain(
546                         self._dialpads.iteritems(),
547                         self._accountViews.iteritems(),
548                         self._messagesViews.iteritems(),
549                         self._recentViews.iteritems(),
550                         self._contactsViews.iteritems(),
551                 ):
552                         sectionName = "%s - %s" % (backendId, view.name())
553                         try:
554                                 view.load_settings(config, sectionName)
555                         except ConfigParser.NoOptionError, e:
556                                 _moduleLogger.exception(
557                                         "Settings file %s is missing section %s" % (
558                                                 constants._user_settings_,
559                                                 e.section,
560                                         ),
561                                 )
562                         except ConfigParser.NoSectionError, e:
563                                 _moduleLogger.exception(
564                                         "Settings file %s is missing section %s" % (
565                                                 constants._user_settings_,
566                                                 e.section,
567                                         ),
568                                 )
569
570                 try:
571                         previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
572                         if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
573                                 hildonize.window_to_landscape(self._window)
574                         elif previousOrientation == gtk.ORIENTATION_VERTICAL:
575                                 hildonize.window_to_portrait(self._window)
576                 except ConfigParser.NoOptionError, e:
577                         _moduleLogger.exception(
578                                 "Settings file %s is missing section %s" % (
579                                         constants._user_settings_,
580                                         e.section,
581                                 ),
582                         )
583                 except ConfigParser.NoSectionError, e:
584                         _moduleLogger.exception(
585                                 "Settings file %s is missing section %s" % (
586                                         constants._user_settings_,
587                                         e.section,
588                                 ),
589                         )
590
591         def save_settings(self, config):
592                 """
593                 @note Thread Agnostic
594                 """
595                 # Because we now only support GVoice, if there are user credentials,
596                 # always assume its using the GVoice backend
597                 if self._credentials[0] and self._credentials[1]:
598                         backend = self.GV_BACKEND
599                 else:
600                         backend = self.NULL_BACKEND
601
602                 config.add_section(constants.__pretty_app_name__)
603                 config.set(constants.__pretty_app_name__, "active", str(backend))
604                 config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
605                 for i, value in enumerate(self._credentials):
606                         blob = base64.b64encode(value)
607                         config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
608                 config.add_section("alarm")
609                 if self._alarmHandler is not None:
610                         self._alarmHandler.save_settings(config, "alarm")
611
612                 for backendId, view in itertools.chain(
613                         self._dialpads.iteritems(),
614                         self._accountViews.iteritems(),
615                         self._messagesViews.iteritems(),
616                         self._recentViews.iteritems(),
617                         self._contactsViews.iteritems(),
618                 ):
619                         sectionName = "%s - %s" % (backendId, view.name())
620                         config.add_section(sectionName)
621                         view.save_settings(config, sectionName)
622
623         def _save_settings(self):
624                 """
625                 @note Thread Agnostic
626                 """
627                 config = ConfigParser.SafeConfigParser()
628                 self.save_settings(config)
629                 with open(constants._user_settings_, "wb") as configFile:
630                         config.write(configFile)
631
632         def _refresh_active_tab(self):
633                 pageIndex = self._notebook.get_current_page()
634                 if pageIndex == self.CONTACTS_TAB:
635                         self._contactsViews[self._selectedBackendId].update(force=True)
636                 elif pageIndex == self.RECENT_TAB:
637                         self._recentViews[self._selectedBackendId].update(force=True)
638                 elif pageIndex == self.MESSAGES_TAB:
639                         self._messagesViews[self._selectedBackendId].update(force=True)
640
641                 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
642                         if self._ledHandler is not None:
643                                 self._ledHandler.off()
644
645         def _on_close(self, *args, **kwds):
646                 try:
647                         if self._osso is not None:
648                                 self._osso.close()
649
650                         if self._initDone:
651                                 self._save_settings()
652                 finally:
653                         gtk.main_quit()
654
655         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
656                 """
657                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
658                 For system_inactivity, we have no background tasks to pause
659
660                 @note Hildon specific
661                 """
662                 try:
663                         if memory_low:
664                                 for backendId in self.BACKENDS:
665                                         self._phoneBackends[backendId].clear_caches()
666                                 self._contactsViews[self._selectedBackendId].clear_caches()
667                                 gc.collect()
668
669                         if save_unsaved_data or shutdown:
670                                 self._save_settings()
671                 except Exception, e:
672                         self._errorDisplay.push_exception()
673
674         def _on_connection_change(self, connection, event, magicIdentifier):
675                 """
676                 @note Hildon specific
677                 """
678                 try:
679                         import conic
680
681                         status = event.get_status()
682                         error = event.get_error()
683                         iap_id = event.get_iap_id()
684                         bearer = event.get_bearer_type()
685
686                         if status == conic.STATUS_CONNECTED:
687                                 if self._initDone:
688                                         self._spawn_attempt_login()
689                         elif status == conic.STATUS_DISCONNECTED:
690                                 if self._initDone:
691                                         self._defaultBackendId = self._selectedBackendId
692                                         self._change_loggedin_status(self.NULL_BACKEND)
693                 except Exception, e:
694                         self._errorDisplay.push_exception()
695
696         def _on_window_state_change(self, widget, event, *args):
697                 """
698                 @note Hildon specific
699                 """
700                 try:
701                         if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
702                                 self._isFullScreen = True
703                         else:
704                                 self._isFullScreen = False
705                 except Exception, e:
706                         self._errorDisplay.push_exception()
707
708         def _on_key_press(self, widget, event, *args):
709                 """
710                 @note Hildon specific
711                 """
712                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
713                 try:
714                         if (
715                                 event.keyval == gtk.keysyms.F6 or
716                                 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
717                         ):
718                                 if self._isFullScreen:
719                                         self._window.unfullscreen()
720                                 else:
721                                         self._window.fullscreen()
722                 except Exception, e:
723                         self._errorDisplay.push_exception()
724
725         def _on_clearcookies_clicked(self, *args):
726                 try:
727                         self._phoneBackends[self._selectedBackendId].logout()
728                         self._accountViews[self._selectedBackendId].clear()
729                         self._recentViews[self._selectedBackendId].clear()
730                         self._messagesViews[self._selectedBackendId].clear()
731                         self._contactsViews[self._selectedBackendId].clear()
732                         self._change_loggedin_status(self.NULL_BACKEND)
733
734                         self._spawn_attempt_login(True)
735                 except Exception, e:
736                         self._errorDisplay.push_exception()
737
738         def _on_notebook_switch_page(self, notebook, page, pageIndex):
739                 try:
740                         self._reset_tab_refresh()
741
742                         didRecentUpdate = False
743                         didMessagesUpdate = False
744
745                         if pageIndex == self.RECENT_TAB:
746                                 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
747                         elif pageIndex == self.MESSAGES_TAB:
748                                 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
749                         elif pageIndex == self.CONTACTS_TAB:
750                                 self._contactsViews[self._selectedBackendId].update()
751                         elif pageIndex == self.ACCOUNT_TAB:
752                                 self._accountViews[self._selectedBackendId].update()
753
754                         if didRecentUpdate or didMessagesUpdate:
755                                 if self._ledHandler is not None:
756                                         self._ledHandler.off()
757                 except Exception, e:
758                         self._errorDisplay.push_exception()
759
760         def _set_tab_refresh(self, *args):
761                 try:
762                         pageIndex = self._notebook.get_current_page()
763                         child = self._notebook.get_nth_page(pageIndex)
764                         self._notebook.get_tab_label(child).set_text("Refresh?")
765                 except Exception, e:
766                         self._errorDisplay.push_exception()
767                 return False
768
769         def _reset_tab_refresh(self, *args):
770                 try:
771                         pageIndex = self._notebook.get_current_page()
772                         child = self._notebook.get_nth_page(pageIndex)
773                         self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
774                 except Exception, e:
775                         self._errorDisplay.push_exception()
776                 return False
777
778         def _on_tab_refresh(self, *args):
779                 try:
780                         self._refresh_active_tab()
781                         self._reset_tab_refresh()
782                 except Exception, e:
783                         self._errorDisplay.push_exception()
784                 return False
785
786         def _on_sms_clicked(self, number, message):
787                 try:
788                         assert number, "No number specified"
789                         assert message, "Empty message"
790                         try:
791                                 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
792                         except Exception, e:
793                                 loggedIn = False
794                                 self._errorDisplay.push_exception()
795                                 return
796
797                         if not loggedIn:
798                                 self._errorDisplay.push_message(
799                                         "Backend link with GoogleVoice is not working, please try again"
800                                 )
801                                 return
802
803                         dialed = False
804                         try:
805                                 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
806                                 hildonize.show_information_banner(self._window, "Sending to %s" % number)
807                                 dialed = True
808                         except Exception, e:
809                                 self._errorDisplay.push_exception()
810
811                         if dialed:
812                                 self._dialpads[self._selectedBackendId].clear()
813                 except Exception, e:
814                         self._errorDisplay.push_exception()
815
816         def _on_dial_clicked(self, number):
817                 try:
818                         assert number, "No number to call"
819                         try:
820                                 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
821                         except Exception, e:
822                                 loggedIn = False
823                                 self._errorDisplay.push_exception()
824                                 return
825
826                         if not loggedIn:
827                                 self._errorDisplay.push_message(
828                                         "Backend link with GoogleVoice is not working, please try again"
829                                 )
830                                 return
831
832                         dialed = False
833                         try:
834                                 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
835                                 self._phoneBackends[self._selectedBackendId].dial(number)
836                                 hildonize.show_information_banner(self._window, "Calling %s" % number)
837                                 dialed = True
838                         except Exception, e:
839                                 self._errorDisplay.push_exception()
840
841                         if dialed:
842                                 self._dialpads[self._selectedBackendId].clear()
843                 except Exception, e:
844                         self._errorDisplay.push_exception()
845
846         def _on_menu_refresh(self, *args):
847                 try:
848                         self._refresh_active_tab()
849                 except Exception, e:
850                         self._errorDisplay.push_exception()
851
852         def _on_paste(self, *args):
853                 try:
854                         contents = self._clipboard.wait_for_text()
855                         if contents is not None:
856                                 self._dialpads[self._selectedBackendId].set_number(contents)
857                 except Exception, e:
858                         self._errorDisplay.push_exception()
859
860         def _on_about_activate(self, *args):
861                 try:
862                         dlg = gtk.AboutDialog()
863                         dlg.set_name(constants.__pretty_app_name__)
864                         dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
865                         dlg.set_copyright("Copyright 2008 - LGPL")
866                         dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account.  This application is not affiliated with Google in any way")
867                         dlg.set_website("http://gc-dialer.garage.maemo.org/")
868                         dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
869                         dlg.run()
870                         dlg.destroy()
871                 except Exception, e:
872                         self._errorDisplay.push_exception()
873
874
875 def run_doctest():
876         import doctest
877
878         failureCount, testCount = doctest.testmod()
879         if not failureCount:
880                 print "Tests Successful"
881                 sys.exit(0)
882         else:
883                 sys.exit(1)
884
885
886 def run_dialpad():
887         _lock_file = os.path.join(constants._data_path_, ".lock")
888         #with gtk_toolbox.flock(_lock_file, 0):
889         gtk.gdk.threads_init()
890
891         if hildonize.IS_HILDON_SUPPORTED:
892                 gtk.set_application_name(constants.__pretty_app_name__)
893         handle = Dialcentral()
894         if not PROFILE_STARTUP:
895                 gtk.main()
896
897
898 class DummyOptions(object):
899
900         def __init__(self):
901                 self.test = False
902
903
904 if __name__ == "__main__":
905         logging.basicConfig(level=logging.DEBUG)
906         try:
907                 if len(sys.argv) > 1:
908                         try:
909                                 import optparse
910                         except ImportError:
911                                 optparse = None
912
913                         if optparse is not None:
914                                 parser = optparse.OptionParser()
915                                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
916                                 (commandOptions, commandArgs) = parser.parse_args()
917                 else:
918                         commandOptions = DummyOptions()
919                         commandArgs = []
920
921                 if commandOptions.test:
922                         run_doctest()
923                 else:
924                         run_dialpad()
925         finally:
926                 logging.shutdown()