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