Adding support for broadcast-sms to the code-base
[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 shutil
33 import logging
34
35 import gtk
36 import gtk.glade
37
38 import constants
39 import hildonize
40 import gtk_toolbox
41
42
43 _moduleLogger = logging.getLogger("dc_glade")
44 PROFILE_STARTUP = False
45
46
47 def getmtime_nothrow(path):
48         try:
49                 return os.path.getmtime(path)
50         except Exception:
51                 return 0
52
53
54 def display_error_message(msg):
55         error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
56
57         def close(dialog, response):
58                 dialog.destroy()
59         error_dialog.connect("response", close)
60         error_dialog.run()
61
62
63 class Dialcentral(object):
64
65         _glade_files = [
66                 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
67                 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
68                 '/usr/lib/dialcentral/dialcentral.glade',
69         ]
70
71         KEYPAD_TAB = 0
72         RECENT_TAB = 1
73         MESSAGES_TAB = 2
74         CONTACTS_TAB = 3
75         ACCOUNT_TAB = 4
76
77         NULL_BACKEND = 0
78         # 1 Was GrandCentral support so the gap was maintained for compatibility
79         GV_BACKEND = 2
80         BACKENDS = (NULL_BACKEND, GV_BACKEND)
81
82         def __init__(self):
83                 self._initDone = False
84                 self._connection = None
85                 self._osso = None
86                 self._deviceState = None
87                 self._clipboard = gtk.clipboard_get()
88
89                 self._credentials = ("", "")
90                 self._selectedBackendId = self.NULL_BACKEND
91                 self._defaultBackendId = self.GV_BACKEND
92                 self._phoneBackends = None
93                 self._dialpads = None
94                 self._accountViews = None
95                 self._messagesViews = None
96                 self._historyViews = None
97                 self._contactsViews = None
98                 self._alarmHandler = None
99                 self._ledHandler = None
100                 self._originalCurrentLabels = []
101                 self._fsContactsPath = os.path.join(constants._data_path_, "contacts")
102
103                 for path in self._glade_files:
104                         if os.path.isfile(path):
105                                 self._widgetTree = gtk.glade.XML(path)
106                                 break
107                 else:
108                         display_error_message("Cannot find dialcentral.glade")
109                         gtk.main_quit()
110                         return
111
112                 self._window = self._widgetTree.get_widget("mainWindow")
113                 self._notebook = self._widgetTree.get_widget("notebook")
114                 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
115                 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
116
117                 self._isFullScreen = False
118                 self._app = hildonize.get_app_class()()
119                 self._window = hildonize.hildonize_window(self._app, self._window)
120                 hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
121                 hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
122
123                 for scrollingWidgetName in (
124                         'history_scrolledwindow',
125                         'message_scrolledwindow',
126                         'contacts_scrolledwindow',
127                         "smsMessages_scrolledwindow",
128                 ):
129                         scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
130                         assert scrollingWidget is not None, scrollingWidgetName
131                         hildonize.hildonize_scrollwindow(scrollingWidget)
132                 for scrollingWidgetName in (
133                         "smsMessage_scrolledEntry",
134                 ):
135                         scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
136                         assert scrollingWidget is not None, scrollingWidgetName
137                         hildonize.hildonize_scrollwindow_with_viewport(scrollingWidget)
138
139                 for buttonName in (
140                         "back",
141                         "addressbookSelectButton",
142                         "sendSmsButton",
143                         "dialButton",
144                         "cancelSmsButton",
145                         "callbackSelectButton",
146                         "minutesEntryButton",
147                         "clearcookies",
148                         "phoneTypeSelection",
149                 ):
150                         button = self._widgetTree.get_widget(buttonName)
151                         assert button is not None, buttonName
152                         hildonize.set_button_thumb_selectable(button)
153
154                 menu = hildonize.hildonize_menu(
155                         self._window,
156                         self._widgetTree.get_widget("dialpad_menubar"),
157                 )
158                 if not hildonize.GTK_MENU_USED:
159                         button = gtk.Button("New Login")
160                         button.connect("clicked", self._on_clearcookies_clicked)
161                         menu.append(button)
162
163                         button = gtk.Button("Import Contacts")
164                         button.connect("clicked", self._on_contact_import)
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                         fileBackend = file_backend.FilesystemAddressBookFactory(self._fsContactsPath)
323
324                         self._dialpads[self.GV_BACKEND].number_selected = self._select_action
325                         self._historyViews[self.GV_BACKEND].number_selected = self._select_action
326                         self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
327                         self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
328
329                         addressBooks = [
330                                 self._phoneBackends[self.GV_BACKEND],
331                                 fileBackend,
332                         ]
333                         mergedBook = merge_backend.MergedAddressBook(addressBooks, merge_backend.MergedAddressBook.basic_firtname_sorter)
334                         self._contactsViews[self.GV_BACKEND].append(mergedBook)
335                         self._contactsViews[self.GV_BACKEND].extend(addressBooks)
336                         self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
337
338                         callbackMapping = {
339                                 "on_paste": self._on_paste,
340                                 "on_refresh": self._on_menu_refresh,
341                                 "on_clearcookies_clicked": self._on_clearcookies_clicked,
342                                 "on_about_activate": self._on_about_activate,
343                                 "on_import": self._on_contact_import,
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, numbers, message):
495                 self.refresh_session()
496                 if action == "dial":
497                         assert len(numbers) == 1
498                         number = numbers[0]
499                         self._on_dial_clicked(number)
500                 elif action == "sms":
501                         self._on_sms_clicked(numbers, message)
502                 else:
503                         assert False, "Unknown action: %s" % action
504
505         def _change_loggedin_status(self, newStatus):
506                 oldStatus = self._selectedBackendId
507                 if oldStatus == newStatus:
508                         return
509
510                 self._dialpads[oldStatus].disable()
511                 self._accountViews[oldStatus].disable()
512                 self._historyViews[oldStatus].disable()
513                 self._messagesViews[oldStatus].disable()
514                 self._contactsViews[oldStatus].disable()
515
516                 self._dialpads[newStatus].enable()
517                 self._accountViews[newStatus].enable()
518                 self._historyViews[newStatus].enable()
519                 self._messagesViews[newStatus].enable()
520                 self._contactsViews[newStatus].enable()
521
522                 self._selectedBackendId = newStatus
523
524                 self._accountViews[self._selectedBackendId].update()
525                 self._refresh_active_tab()
526
527         def load_settings(self, config):
528                 """
529                 @note UI Thread
530                 """
531                 try:
532                         if not PROFILE_STARTUP:
533                                 self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
534                         else:
535                                 self._defaultBackendId = self.NULL_BACKEND
536                         blobs = (
537                                 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
538                                 for i in xrange(len(self._credentials))
539                         )
540                         creds = (
541                                 base64.b64decode(blob)
542                                 for blob in blobs
543                         )
544                         self._credentials = tuple(creds)
545
546                         if self._alarmHandler is not None:
547                                 self._alarmHandler.load_settings(config, "alarm")
548
549                         isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
550                         if isFullscreen:
551                                 self._window.fullscreen()
552                 except ConfigParser.NoOptionError, e:
553                         _moduleLogger.exception(
554                                 "Settings file %s is missing section %s" % (
555                                         constants._user_settings_,
556                                         e.section,
557                                 ),
558                         )
559                 except ConfigParser.NoSectionError, e:
560                         _moduleLogger.exception(
561                                 "Settings file %s is missing section %s" % (
562                                         constants._user_settings_,
563                                         e.section,
564                                 ),
565                         )
566
567                 for backendId, view in itertools.chain(
568                         self._dialpads.iteritems(),
569                         self._accountViews.iteritems(),
570                         self._messagesViews.iteritems(),
571                         self._historyViews.iteritems(),
572                         self._contactsViews.iteritems(),
573                 ):
574                         sectionName = "%s - %s" % (backendId, view.name())
575                         try:
576                                 view.load_settings(config, sectionName)
577                         except ConfigParser.NoOptionError, e:
578                                 _moduleLogger.exception(
579                                         "Settings file %s is missing section %s" % (
580                                                 constants._user_settings_,
581                                                 e.section,
582                                         ),
583                                 )
584                         except ConfigParser.NoSectionError, e:
585                                 _moduleLogger.exception(
586                                         "Settings file %s is missing section %s" % (
587                                                 constants._user_settings_,
588                                                 e.section,
589                                         ),
590                                 )
591
592                 try:
593                         previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
594                         if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
595                                 hildonize.window_to_landscape(self._window)
596                         elif previousOrientation == gtk.ORIENTATION_VERTICAL:
597                                 hildonize.window_to_portrait(self._window)
598                 except ConfigParser.NoOptionError, e:
599                         _moduleLogger.exception(
600                                 "Settings file %s is missing section %s" % (
601                                         constants._user_settings_,
602                                         e.section,
603                                 ),
604                         )
605                 except ConfigParser.NoSectionError, e:
606                         _moduleLogger.exception(
607                                 "Settings file %s is missing section %s" % (
608                                         constants._user_settings_,
609                                         e.section,
610                                 ),
611                         )
612
613         def save_settings(self, config):
614                 """
615                 @note Thread Agnostic
616                 """
617                 # Because we now only support GVoice, if there are user credentials,
618                 # always assume its using the GVoice backend
619                 if self._credentials[0] and self._credentials[1]:
620                         backend = self.GV_BACKEND
621                 else:
622                         backend = self.NULL_BACKEND
623
624                 config.add_section(constants.__pretty_app_name__)
625                 config.set(constants.__pretty_app_name__, "active", str(backend))
626                 config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
627                 config.set(constants.__pretty_app_name__, "fullscreen", str(self._isFullScreen))
628                 for i, value in enumerate(self._credentials):
629                         blob = base64.b64encode(value)
630                         config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
631                 config.add_section("alarm")
632                 if self._alarmHandler is not None:
633                         self._alarmHandler.save_settings(config, "alarm")
634
635                 for backendId, view in itertools.chain(
636                         self._dialpads.iteritems(),
637                         self._accountViews.iteritems(),
638                         self._messagesViews.iteritems(),
639                         self._historyViews.iteritems(),
640                         self._contactsViews.iteritems(),
641                 ):
642                         sectionName = "%s - %s" % (backendId, view.name())
643                         config.add_section(sectionName)
644                         view.save_settings(config, sectionName)
645
646         def _save_settings(self):
647                 """
648                 @note Thread Agnostic
649                 """
650                 config = ConfigParser.SafeConfigParser()
651                 self.save_settings(config)
652                 with open(constants._user_settings_, "wb") as configFile:
653                         config.write(configFile)
654
655         def _refresh_active_tab(self):
656                 pageIndex = self._notebook.get_current_page()
657                 if pageIndex == self.CONTACTS_TAB:
658                         self._contactsViews[self._selectedBackendId].update(force=True)
659                 elif pageIndex == self.RECENT_TAB:
660                         self._historyViews[self._selectedBackendId].update(force=True)
661                 elif pageIndex == self.MESSAGES_TAB:
662                         self._messagesViews[self._selectedBackendId].update(force=True)
663
664                 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
665                         if self._ledHandler is not None:
666                                 self._ledHandler.off()
667
668         @gtk_toolbox.log_exception(_moduleLogger)
669         def _on_close(self, *args, **kwds):
670                 try:
671                         if self._initDone:
672                                 self._save_settings()
673
674                         try:
675                                 self._deviceState.close()
676                         except AttributeError:
677                                 pass # Either None or close was removed (in Fremantle)
678                         try:
679                                 self._osso.close()
680                         except AttributeError:
681                                 pass # Either None or close was removed (in Fremantle)
682                 finally:
683                         gtk.main_quit()
684
685         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
686                 """
687                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
688                 For system_inactivity, we have no background tasks to pause
689
690                 @note Hildon specific
691                 """
692                 try:
693                         if memory_low:
694                                 for backendId in self.BACKENDS:
695                                         self._phoneBackends[backendId].clear_caches()
696                                 self._contactsViews[self._selectedBackendId].clear_caches()
697                                 gc.collect()
698
699                         if save_unsaved_data or shutdown:
700                                 self._save_settings()
701                 except Exception, e:
702                         self._errorDisplay.push_exception()
703
704         def _on_connection_change(self, connection, event, magicIdentifier):
705                 """
706                 @note Hildon specific
707                 """
708                 try:
709                         import conic
710
711                         status = event.get_status()
712                         error = event.get_error()
713                         iap_id = event.get_iap_id()
714                         bearer = event.get_bearer_type()
715
716                         if status == conic.STATUS_CONNECTED:
717                                 if self._initDone:
718                                         self._spawn_attempt_login()
719                         elif status == conic.STATUS_DISCONNECTED:
720                                 if self._initDone:
721                                         self._defaultBackendId = self._selectedBackendId
722                                         self._change_loggedin_status(self.NULL_BACKEND)
723                 except Exception, e:
724                         self._errorDisplay.push_exception()
725
726         def _on_window_state_change(self, widget, event, *args):
727                 """
728                 @note Hildon specific
729                 """
730                 try:
731                         if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
732                                 self._isFullScreen = True
733                         else:
734                                 self._isFullScreen = False
735                 except Exception, e:
736                         self._errorDisplay.push_exception()
737
738         def _on_key_press(self, widget, event, *args):
739                 """
740                 @note Hildon specific
741                 """
742                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
743                 try:
744                         if (
745                                 event.keyval == gtk.keysyms.F6 or
746                                 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
747                         ):
748                                 if self._isFullScreen:
749                                         self._window.unfullscreen()
750                                 else:
751                                         self._window.fullscreen()
752                         elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK:
753                                 with open(constants._user_logpath_, "r") as f:
754                                         logLines = f.xreadlines()
755                                         log = "".join(logLines)
756                                         self._clipboard.set_text(str(log))
757                         elif (
758                                 event.keyval in (gtk.keysyms.w, gtk.keysyms.q) and
759                                 event.get_state() & gtk.gdk.CONTROL_MASK
760                         ):
761                                 self._window.destroy()
762                         elif event.keyval == gtk.keysyms.r and event.get_state() & gtk.gdk.CONTROL_MASK:
763                                 self._refresh_active_tab()
764                 except Exception, e:
765                         self._errorDisplay.push_exception()
766
767         def _on_clearcookies_clicked(self, *args):
768                 try:
769                         self._phoneBackends[self._selectedBackendId].logout()
770                         self._accountViews[self._selectedBackendId].clear()
771                         self._historyViews[self._selectedBackendId].clear()
772                         self._messagesViews[self._selectedBackendId].clear()
773                         self._contactsViews[self._selectedBackendId].clear()
774                         self._change_loggedin_status(self.NULL_BACKEND)
775
776                         self._spawn_attempt_login(True)
777                 except Exception, e:
778                         self._errorDisplay.push_exception()
779
780         def _on_notebook_switch_page(self, notebook, page, pageIndex):
781                 try:
782                         self._reset_tab_refresh()
783
784                         didRecentUpdate = False
785                         didMessagesUpdate = False
786
787                         if pageIndex == self.RECENT_TAB:
788                                 didRecentUpdate = self._historyViews[self._selectedBackendId].update()
789                         elif pageIndex == self.MESSAGES_TAB:
790                                 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
791                         elif pageIndex == self.CONTACTS_TAB:
792                                 self._contactsViews[self._selectedBackendId].update()
793                         elif pageIndex == self.ACCOUNT_TAB:
794                                 self._accountViews[self._selectedBackendId].update()
795
796                         if didRecentUpdate or didMessagesUpdate:
797                                 if self._ledHandler is not None:
798                                         self._ledHandler.off()
799                 except Exception, e:
800                         self._errorDisplay.push_exception()
801
802         def _set_tab_refresh(self, *args):
803                 try:
804                         pageIndex = self._notebook.get_current_page()
805                         child = self._notebook.get_nth_page(pageIndex)
806                         self._notebook.get_tab_label(child).set_text("Refresh?")
807                 except Exception, e:
808                         self._errorDisplay.push_exception()
809                 return False
810
811         def _reset_tab_refresh(self, *args):
812                 try:
813                         pageIndex = self._notebook.get_current_page()
814                         child = self._notebook.get_nth_page(pageIndex)
815                         self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
816                 except Exception, e:
817                         self._errorDisplay.push_exception()
818                 return False
819
820         def _on_tab_refresh(self, *args):
821                 try:
822                         self._refresh_active_tab()
823                         self._reset_tab_refresh()
824                 except Exception, e:
825                         self._errorDisplay.push_exception()
826                 return False
827
828         def _on_sms_clicked(self, numbers, message):
829                 try:
830                         assert numbers, "No number specified"
831                         assert message, "Empty message"
832                         try:
833                                 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
834                         except Exception, e:
835                                 loggedIn = False
836                                 self._errorDisplay.push_exception()
837                                 return
838
839                         if not loggedIn:
840                                 self._errorDisplay.push_message(
841                                         "Backend link with GoogleVoice is not working, please try again"
842                                 )
843                                 return
844
845                         dialed = False
846                         try:
847                                 self._phoneBackends[self._selectedBackendId].send_sms(numbers, message)
848                                 hildonize.show_information_banner(self._window, "Sending to %s" % ", ".join(numbers))
849                                 _moduleLogger.info("Sending SMS to %r" % numbers)
850                                 dialed = True
851                         except Exception, e:
852                                 self._errorDisplay.push_exception()
853
854                         if dialed:
855                                 self._dialpads[self._selectedBackendId].clear()
856                 except Exception, e:
857                         self._errorDisplay.push_exception()
858
859         def _on_dial_clicked(self, number):
860                 try:
861                         assert number, "No number to call"
862                         try:
863                                 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
864                         except Exception, e:
865                                 loggedIn = False
866                                 self._errorDisplay.push_exception()
867                                 return
868
869                         if not loggedIn:
870                                 self._errorDisplay.push_message(
871                                         "Backend link with GoogleVoice is not working, please try again"
872                                 )
873                                 return
874
875                         dialed = False
876                         try:
877                                 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
878                                 self._phoneBackends[self._selectedBackendId].call(number)
879                                 hildonize.show_information_banner(self._window, "Calling %s" % number)
880                                 _moduleLogger.info("Calling %s" % number)
881                                 dialed = True
882                         except Exception, e:
883                                 self._errorDisplay.push_exception()
884
885                         if dialed:
886                                 self._dialpads[self._selectedBackendId].clear()
887                 except Exception, e:
888                         self._errorDisplay.push_exception()
889
890         def _on_contact_import(self, *args):
891                 try:
892                         csvFilter = gtk.FileFilter()
893                         csvFilter.set_name("Contacts")
894                         csvFilter.add_pattern("*.csv")
895                         importFileChooser = gtk.FileChooserDialog(
896                                 title="Contacts",
897                                 parent=self._window,
898                         )
899                         importFileChooser.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
900                         importFileChooser.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
901
902                         importFileChooser.set_property("filter", csvFilter)
903                         userResponse = importFileChooser.run()
904                         importFileChooser.hide()
905                         if userResponse == gtk.RESPONSE_OK:
906                                 filename = importFileChooser.get_filename()
907                                 shutil.copy2(filename, self._fsContactsPath)
908                 except Exception, e:
909                         self._errorDisplay.push_exception()
910
911         def _on_menu_refresh(self, *args):
912                 try:
913                         self._refresh_active_tab()
914                 except Exception, e:
915                         self._errorDisplay.push_exception()
916
917         def _on_paste(self, *args):
918                 try:
919                         contents = self._clipboard.wait_for_text()
920                         if contents is not None:
921                                 self._dialpads[self._selectedBackendId].set_number(contents)
922                 except Exception, e:
923                         self._errorDisplay.push_exception()
924
925         def _on_about_activate(self, *args):
926                 try:
927                         dlg = gtk.AboutDialog()
928                         dlg.set_name(constants.__pretty_app_name__)
929                         dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
930                         dlg.set_copyright("Copyright 2008 - LGPL")
931                         dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account.  This application is not affiliated with Google in any way")
932                         dlg.set_website("http://gc-dialer.garage.maemo.org/")
933                         dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <eopage@byu.net>"])
934                         dlg.run()
935                         dlg.destroy()
936                 except Exception, e:
937                         self._errorDisplay.push_exception()
938
939
940 def run_doctest():
941         import doctest
942
943         failureCount, testCount = doctest.testmod()
944         if not failureCount:
945                 print "Tests Successful"
946                 sys.exit(0)
947         else:
948                 sys.exit(1)
949
950
951 def run_dialpad():
952         _lock_file = os.path.join(constants._data_path_, ".lock")
953         #with gtk_toolbox.flock(_lock_file, 0):
954         gtk.gdk.threads_init()
955
956         if hildonize.IS_HILDON_SUPPORTED:
957                 gtk.set_application_name(constants.__pretty_app_name__)
958         handle = Dialcentral()
959         if not PROFILE_STARTUP:
960                 gtk.main()
961
962
963 class DummyOptions(object):
964
965         def __init__(self):
966                 self.test = False
967
968
969 if __name__ == "__main__":
970         logging.basicConfig(level=logging.DEBUG)
971         try:
972                 if len(sys.argv) > 1:
973                         try:
974                                 import optparse
975                         except ImportError:
976                                 optparse = None
977
978                         if optparse is not None:
979                                 parser = optparse.OptionParser()
980                                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
981                                 (commandOptions, commandArgs) = parser.parse_args()
982                 else:
983                         commandOptions = DummyOptions()
984                         commandArgs = []
985
986                 if commandOptions.test:
987                         run_doctest()
988                 else:
989                         run_dialpad()
990         finally:
991                 logging.shutdown()