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