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