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