Merging in files from skeleton
[gc-dialer] / src / dc_glade.py
1 #!/usr/bin/env python
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.__isPortrait = False
123                 self._app = hildonize.get_app_class()()
124                 self._window = hildonize.hildonize_window(self._app, self._window)
125                 hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
126                 hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
127
128                 for scrollingWidgetName in (
129                         'history_scrolledwindow',
130                         'message_scrolledwindow',
131                         'contacts_scrolledwindow',
132                         "smsMessages_scrolledwindow",
133                 ):
134                         scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
135                         assert scrollingWidget is not None, scrollingWidgetName
136                         hildonize.hildonize_scrollwindow(scrollingWidget)
137                 for scrollingWidgetName in (
138                         "smsMessage_scrolledEntry",
139                 ):
140                         scrollingWidget = self._widgetTree.get_widget(scrollingWidgetName)
141                         assert scrollingWidget is not None, scrollingWidgetName
142                         hildonize.hildonize_scrollwindow_with_viewport(scrollingWidget)
143
144                 for buttonName in (
145                         "back",
146                         "addressbookSelectButton",
147                         "sendSmsButton",
148                         "dialButton",
149                         "callbackSelectButton",
150                         "minutesEntryButton",
151                         "clearcookies",
152                         "phoneTypeSelection",
153                 ):
154                         button = self._widgetTree.get_widget(buttonName)
155                         assert button is not None, buttonName
156                         hildonize.set_button_thumb_selectable(button)
157
158                 menu = hildonize.hildonize_menu(
159                         self._window,
160                         self._widgetTree.get_widget("dialpad_menubar"),
161                 )
162                 if not hildonize.GTK_MENU_USED:
163                         button = gtk.Button("New Login")
164                         button.connect("clicked", self._on_clearcookies_clicked)
165                         menu.append(button)
166
167                         button = gtk.Button("Refresh")
168                         button.connect("clicked", self._on_menu_refresh)
169                         menu.append(button)
170
171                         menu.show_all()
172
173                 self._window.connect("key-press-event", self._on_key_press)
174                 self._window.connect("window-state-event", self._on_window_state_change)
175                 if not hildonize.IS_HILDON_SUPPORTED:
176                         _moduleLogger.warning("No hildonization support")
177
178                 hildonize.set_application_name("%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                         with gtk_toolbox.gtk_lock():
286                                 self._smsEntryWindow = gv_views.SmsEntryWindow(self._widgetTree, self._window, self._app)
287                         try:
288                                 os.makedirs(constants._data_path_)
289                         except OSError, e:
290                                 if e.errno != 17:
291                                         raise
292                         gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
293
294                         self._phoneBackends.update({
295                                 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
296                         })
297                         with gtk_toolbox.gtk_lock():
298                                 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
299                                 self._dialpads.update({
300                                         self.GV_BACKEND: unifiedDialpad,
301                                 })
302                                 self._accountViews.update({
303                                         self.GV_BACKEND: gv_views.AccountInfo(
304                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
305                                         ),
306                                 })
307                                 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
308                                 self._historyViews.update({
309                                         self.GV_BACKEND: gv_views.CallHistoryView(
310                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
311                                         ),
312                                 })
313                                 self._messagesViews.update({
314                                         self.GV_BACKEND: gv_views.MessagesView(
315                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
316                                         ),
317                                 })
318                                 self._contactsViews.update({
319                                         self.GV_BACKEND: gv_views.ContactsView(
320                                                 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
321                                         ),
322                                 })
323
324                         fileBackend = file_backend.FilesystemAddressBookFactory(self._fsContactsPath)
325
326                         self._smsEntryWindow.send_sms = self._on_sms_clicked
327                         self._smsEntryWindow.dial = self._on_dial_clicked
328                         self._dialpads[self.GV_BACKEND].add_contact = self._add_contact
329                         self._dialpads[self.GV_BACKEND].dial = self._on_dial_clicked
330                         self._historyViews[self.GV_BACKEND].add_contact = self._add_contact
331                         self._messagesViews[self.GV_BACKEND].add_contact = self._add_contact
332                         self._contactsViews[self.GV_BACKEND].add_contact = self._add_contact
333
334                         addressBooks = [
335                                 self._phoneBackends[self.GV_BACKEND],
336                                 fileBackend,
337                         ]
338                         mergedBook = merge_backend.MergedAddressBook(addressBooks, merge_backend.MergedAddressBook.basic_firtname_sorter)
339                         self._contactsViews[self.GV_BACKEND].append(mergedBook)
340                         self._contactsViews[self.GV_BACKEND].extend(addressBooks)
341                         self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
342
343                         callbackMapping = {
344                                 "on_paste": self._on_paste,
345                                 "on_refresh": self._on_menu_refresh,
346                                 "on_clearcookies_clicked": self._on_clearcookies_clicked,
347                                 "on_about_activate": self._on_about_activate,
348                         }
349                         if hildonize.GTK_MENU_USED:
350                                 self._widgetTree.signal_autoconnect(callbackMapping)
351                         self._notebook.connect("switch-page", self._on_notebook_switch_page)
352                         self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked)
353
354                         with gtk_toolbox.gtk_lock():
355                                 self._originalCurrentLabels = [
356                                         self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
357                                         for pageIndex in xrange(self._notebook.get_n_pages())
358                                 ]
359                                 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
360                                 self._notebookTapHandler.enable()
361                         self._notebookTapHandler.on_tap = self._reset_tab_refresh
362                         self._notebookTapHandler.on_hold = self._on_tab_refresh
363                         self._notebookTapHandler.on_holding = self._set_tab_refresh
364                         self._notebookTapHandler.on_cancel = self._reset_tab_refresh
365
366                         config = ConfigParser.SafeConfigParser()
367                         config.read(constants._user_settings_)
368                         with gtk_toolbox.gtk_lock():
369                                 self.load_settings(config)
370                 except Exception, e:
371                         with gtk_toolbox.gtk_lock():
372                                 self._errorDisplay.push_exception()
373                 finally:
374                         self._initDone = True
375                         self._spawn_attempt_login()
376
377         def _spawn_attempt_login(self, *args):
378                 self._loginSink.send(args)
379
380         def _attempt_login(self, force = False):
381                 """
382                 @note This must be run outside of the UI lock
383                 """
384                 try:
385                         assert self._initDone, "Attempting login before app is fully loaded"
386
387                         serviceId = self.NULL_BACKEND
388                         loggedIn = False
389                         if not force and self._defaultBackendId != self.NULL_BACKEND:
390                                 with gtk_toolbox.gtk_lock():
391                                         banner = hildonize.show_busy_banner_start(self._window, "Logging In...")
392                                 try:
393                                         self.refresh_session()
394                                         serviceId = self._defaultBackendId
395                                         loggedIn = True
396                                 except Exception, e:
397                                         _moduleLogger.exception('Session refresh failed with the following message "%s"' % str(e))
398                                 finally:
399                                         with gtk_toolbox.gtk_lock():
400                                                 hildonize.show_busy_banner_end(banner)
401
402                         if not loggedIn:
403                                 loggedIn, serviceId = self._login_by_user()
404
405                         with gtk_toolbox.gtk_lock():
406                                 self._change_loggedin_status(serviceId)
407                                 if loggedIn:
408                                         hildonize.show_information_banner(self._window, "Logged In")
409                                 else:
410                                         hildonize.show_information_banner(self._window, "Login Failed")
411                                 if not self._phoneBackends[self._defaultBackendId].get_callback_number():
412                                         # subtle reminder to the users to configure things
413                                         self._notebook.set_current_page(self.ACCOUNT_TAB)
414
415                 except Exception, e:
416                         with gtk_toolbox.gtk_lock():
417                                 self._errorDisplay.push_exception()
418
419         def refresh_session(self):
420                 """
421                 @note Thread agnostic
422                 """
423                 assert self._initDone, "Attempting login before app is fully loaded"
424
425                 loggedIn = False
426                 if not loggedIn:
427                         loggedIn = self._login_by_cookie()
428                 if not loggedIn:
429                         loggedIn = self._login_by_settings()
430
431                 if not loggedIn:
432                         raise RuntimeError("Login Failed")
433
434         def _login_by_cookie(self):
435                 """
436                 @note Thread agnostic
437                 """
438                 loggedIn = False
439
440                 isQuickLoginPossible = self._phoneBackends[self._defaultBackendId].is_quick_login_possible()
441                 if self._credentials != ("", "") and isQuickLoginPossible:
442                         if not loggedIn:
443                                 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
444
445                 if loggedIn:
446                         _moduleLogger.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
447                 else:
448                         # If the cookies are bad, scratch them completely
449                         self._phoneBackends[self._defaultBackendId].logout()
450
451                 return loggedIn
452
453         def _login_by_settings(self):
454                 """
455                 @note Thread agnostic
456                 """
457                 if self._credentials == ("", ""):
458                         # Don't bother with the settings if they are blank
459                         return False
460
461                 username, password = self._credentials
462                 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
463                 if loggedIn:
464                         self._credentials = username, password
465                         _moduleLogger.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
466                 return loggedIn
467
468         def _login_by_user(self):
469                 """
470                 @note This must be run outside of the UI lock
471                 """
472                 loggedIn, (username, password) = False, self._credentials
473                 tmpServiceId = self.GV_BACKEND
474                 while not loggedIn:
475                         with gtk_toolbox.gtk_lock():
476                                 credentials = self._credentialsDialog.request_credentials(
477                                         defaultCredentials = self._credentials
478                                 )
479                                 banner = hildonize.show_busy_banner_start(self._window, "Logging In...")
480                         try:
481                                 username, password = credentials
482                                 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
483                         finally:
484                                 with gtk_toolbox.gtk_lock():
485                                         hildonize.show_busy_banner_end(banner)
486
487                 if loggedIn:
488                         serviceId = tmpServiceId
489                         self._credentials = username, password
490                         _moduleLogger.info("Logged into %r through user request" % self._phoneBackends[serviceId])
491                 else:
492                         # Hint to the user that they are not logged in
493                         serviceId = self.NULL_BACKEND
494                         self._notebook.set_current_page(self.ACCOUNT_TAB)
495
496                 return loggedIn, serviceId
497
498         def _add_contact(self, *args, **kwds):
499                 self._smsEntryWindow.add_contact(*args, **kwds)
500
501         def _change_loggedin_status(self, newStatus):
502                 oldStatus = self._selectedBackendId
503                 if oldStatus == newStatus:
504                         return
505
506                 _moduleLogger.debug("Changing from %s to %s" % (oldStatus, newStatus))
507                 self._dialpads[oldStatus].disable()
508                 self._accountViews[oldStatus].disable()
509                 self._historyViews[oldStatus].disable()
510                 self._messagesViews[oldStatus].disable()
511                 self._contactsViews[oldStatus].disable()
512
513                 self._dialpads[newStatus].enable()
514                 self._accountViews[newStatus].enable()
515                 self._historyViews[newStatus].enable()
516                 self._messagesViews[newStatus].enable()
517                 self._contactsViews[newStatus].enable()
518
519                 self._selectedBackendId = newStatus
520
521                 self._accountViews[self._selectedBackendId].update()
522                 self._refresh_active_tab()
523                 self._refresh_orientation()
524
525         def load_settings(self, config):
526                 """
527                 @note UI Thread
528                 """
529                 try:
530                         if not PROFILE_STARTUP:
531                                 self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
532                         else:
533                                 self._defaultBackendId = self.NULL_BACKEND
534                         blobs = (
535                                 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
536                                 for i in xrange(len(self._credentials))
537                         )
538                         creds = (
539                                 base64.b64decode(blob)
540                                 for blob in blobs
541                         )
542                         self._credentials = tuple(creds)
543
544                         if self._alarmHandler is not None:
545                                 self._alarmHandler.load_settings(config, "alarm")
546
547                         isFullscreen = config.getboolean(constants.__pretty_app_name__, "fullscreen")
548                         if isFullscreen:
549                                 self._window.fullscreen()
550
551                         isPortrait = config.getboolean(constants.__pretty_app_name__, "portrait")
552                         if isPortrait ^ self.__isPortrait:
553                                 if isPortrait:
554                                         orientation = gtk.ORIENTATION_VERTICAL
555                                 else:
556                                         orientation = gtk.ORIENTATION_HORIZONTAL
557                                 self.set_orientation(orientation)
558                 except ConfigParser.NoOptionError, e:
559                         _moduleLogger.exception(
560                                 "Settings file %s is missing section %s" % (
561                                         constants._user_settings_,
562                                         e.section,
563                                 ),
564                         )
565                 except ConfigParser.NoSectionError, e:
566                         _moduleLogger.exception(
567                                 "Settings file %s is missing section %s" % (
568                                         constants._user_settings_,
569                                         e.section,
570                                 ),
571                         )
572
573                 for backendId, view in itertools.chain(
574                         self._dialpads.iteritems(),
575                         self._accountViews.iteritems(),
576                         self._messagesViews.iteritems(),
577                         self._historyViews.iteritems(),
578                         self._contactsViews.iteritems(),
579                 ):
580                         sectionName = "%s - %s" % (backendId, view.name())
581                         try:
582                                 view.load_settings(config, sectionName)
583                         except ConfigParser.NoOptionError, e:
584                                 _moduleLogger.exception(
585                                         "Settings file %s is missing section %s" % (
586                                                 constants._user_settings_,
587                                                 e.section,
588                                         ),
589                                 )
590                         except ConfigParser.NoSectionError, e:
591                                 _moduleLogger.exception(
592                                         "Settings file %s is missing section %s" % (
593                                                 constants._user_settings_,
594                                                 e.section,
595                                         ),
596                                 )
597
598         def save_settings(self, config):
599                 """
600                 @note Thread Agnostic
601                 """
602                 # Because we now only support GVoice, if there are user credentials,
603                 # always assume its using the GVoice backend
604                 if self._credentials[0] and self._credentials[1]:
605                         backend = self.GV_BACKEND
606                 else:
607                         backend = self.NULL_BACKEND
608
609                 config.add_section(constants.__pretty_app_name__)
610                 config.set(constants.__pretty_app_name__, "active", str(backend))
611                 config.set(constants.__pretty_app_name__, "portrait", str(self.__isPortrait))
612                 config.set(constants.__pretty_app_name__, "fullscreen", str(self._isFullScreen))
613                 for i, value in enumerate(self._credentials):
614                         blob = base64.b64encode(value)
615                         config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
616                 config.add_section("alarm")
617                 if self._alarmHandler is not None:
618                         self._alarmHandler.save_settings(config, "alarm")
619
620                 for backendId, view in itertools.chain(
621                         self._dialpads.iteritems(),
622                         self._accountViews.iteritems(),
623                         self._messagesViews.iteritems(),
624                         self._historyViews.iteritems(),
625                         self._contactsViews.iteritems(),
626                 ):
627                         sectionName = "%s - %s" % (backendId, view.name())
628                         config.add_section(sectionName)
629                         view.save_settings(config, sectionName)
630
631         def _save_settings(self):
632                 """
633                 @note Thread Agnostic
634                 """
635                 config = ConfigParser.SafeConfigParser()
636                 self.save_settings(config)
637                 with open(constants._user_settings_, "wb") as configFile:
638                         config.write(configFile)
639
640         def _refresh_active_tab(self):
641                 pageIndex = self._notebook.get_current_page()
642                 if pageIndex == self.CONTACTS_TAB:
643                         self._contactsViews[self._selectedBackendId].update(force=True)
644                 elif pageIndex == self.RECENT_TAB:
645                         self._historyViews[self._selectedBackendId].update(force=True)
646                 elif pageIndex == self.MESSAGES_TAB:
647                         self._messagesViews[self._selectedBackendId].update(force=True)
648
649                 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
650                         if self._ledHandler is not None:
651                                 self._ledHandler.off()
652
653         def set_orientation(self, orientation):
654                 if orientation == gtk.ORIENTATION_VERTICAL:
655                         hildonize.window_to_portrait(self._window)
656                         self._notebook.set_property("tab-pos", gtk.POS_BOTTOM)
657                         self.__isPortrait = True
658                 elif orientation == gtk.ORIENTATION_HORIZONTAL:
659                         hildonize.window_to_landscape(self._window)
660                         self._notebook.set_property("tab-pos", gtk.POS_LEFT)
661                         self.__isPortrait = False
662                 else:
663                         raise NotImplementedError(orientation)
664
665         def get_orientation(self):
666                 return gtk.ORIENTATION_VERTICAL if self.__isPortrait else gtk.ORIENTATION_HORIZONTAL
667
668         def _toggle_rotate(self):
669                 if self.__isPortrait:
670                         self.set_orientation(gtk.ORIENTATION_HORIZONTAL)
671                 else:
672                         self.set_orientation(gtk.ORIENTATION_VERTICAL)
673
674         def _refresh_orientation(self):
675                 """
676                 Mostly meant to be used when switching backends
677                 """
678                 if self.__isPortrait:
679                         self.set_orientation(gtk.ORIENTATION_VERTICAL)
680                 else:
681                         self.set_orientation(gtk.ORIENTATION_HORIZONTAL)
682
683         @gtk_toolbox.log_exception(_moduleLogger)
684         def _on_close(self, *args, **kwds):
685                 try:
686                         if self._initDone:
687                                 self._save_settings()
688
689                         try:
690                                 self._deviceState.close()
691                         except AttributeError:
692                                 pass # Either None or close was removed (in Fremantle)
693                         try:
694                                 self._osso.close()
695                         except AttributeError:
696                                 pass # Either None or close was removed (in Fremantle)
697                 finally:
698                         gtk.main_quit()
699
700         def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
701                 """
702                 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
703                 For system_inactivity, we have no background tasks to pause
704
705                 @note Hildon specific
706                 """
707                 try:
708                         if memory_low:
709                                 for backendId in self.BACKENDS:
710                                         self._phoneBackends[backendId].clear_caches()
711                                 self._contactsViews[self._selectedBackendId].clear_caches()
712                                 gc.collect()
713
714                         if save_unsaved_data or shutdown:
715                                 self._save_settings()
716                 except Exception, e:
717                         self._errorDisplay.push_exception()
718
719         def _on_connection_change(self, connection, event, magicIdentifier):
720                 """
721                 @note Hildon specific
722                 """
723                 try:
724                         import conic
725
726                         status = event.get_status()
727                         error = event.get_error()
728                         iap_id = event.get_iap_id()
729                         bearer = event.get_bearer_type()
730
731                         if status == conic.STATUS_CONNECTED:
732                                 if self._initDone:
733                                         self._spawn_attempt_login()
734                         elif status == conic.STATUS_DISCONNECTED:
735                                 if self._initDone:
736                                         self._defaultBackendId = self._selectedBackendId
737                                         self._change_loggedin_status(self.NULL_BACKEND)
738                 except Exception, e:
739                         self._errorDisplay.push_exception()
740
741         def _on_window_state_change(self, widget, event, *args):
742                 """
743                 @note Hildon specific
744                 """
745                 try:
746                         if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
747                                 self._isFullScreen = True
748                         else:
749                                 self._isFullScreen = False
750                 except Exception, e:
751                         self._errorDisplay.push_exception()
752
753         def _on_key_press(self, widget, event, *args):
754                 """
755                 @note Hildon specific
756                 """
757                 RETURN_TYPES = (gtk.keysyms.Return, gtk.keysyms.ISO_Enter, gtk.keysyms.KP_Enter)
758                 try:
759                         if (
760                                 event.keyval == gtk.keysyms.F6 or
761                                 event.keyval in RETURN_TYPES and event.get_state() & gtk.gdk.CONTROL_MASK
762                         ):
763                                 if self._isFullScreen:
764                                         self._window.unfullscreen()
765                                 else:
766                                         self._window.fullscreen()
767                         elif event.keyval == gtk.keysyms.l and event.get_state() & gtk.gdk.CONTROL_MASK:
768                                 with open(constants._user_logpath_, "r") as f:
769                                         logLines = f.xreadlines()
770                                         log = "".join(logLines)
771                                         self._clipboard.set_text(str(log))
772                         elif (
773                                 event.keyval in (gtk.keysyms.w, gtk.keysyms.q) and
774                                 event.get_state() & gtk.gdk.CONTROL_MASK
775                         ):
776                                 self._window.destroy()
777                         elif event.keyval == gtk.keysyms.o and event.get_state() & gtk.gdk.CONTROL_MASK:
778                                 self._toggle_rotate()
779                                 return True
780                         elif event.keyval == gtk.keysyms.r and event.get_state() & gtk.gdk.CONTROL_MASK:
781                                 self._refresh_active_tab()
782                         elif event.keyval == gtk.keysyms.i and event.get_state() & gtk.gdk.CONTROL_MASK:
783                                 self._import_contacts()
784                 except Exception, e:
785                         self._errorDisplay.push_exception()
786
787         def _on_clearcookies_clicked(self, *args):
788                 try:
789                         self._phoneBackends[self._selectedBackendId].logout()
790                         self._accountViews[self._selectedBackendId].clear()
791                         self._historyViews[self._selectedBackendId].clear()
792                         self._messagesViews[self._selectedBackendId].clear()
793                         self._contactsViews[self._selectedBackendId].clear()
794                         self._change_loggedin_status(self.NULL_BACKEND)
795
796                         self._spawn_attempt_login(True)
797                 except Exception, e:
798                         self._errorDisplay.push_exception()
799
800         def _on_notebook_switch_page(self, notebook, page, pageIndex):
801                 try:
802                         self._reset_tab_refresh()
803
804                         didRecentUpdate = False
805                         didMessagesUpdate = False
806
807                         if pageIndex == self.RECENT_TAB:
808                                 didRecentUpdate = self._historyViews[self._selectedBackendId].update()
809                         elif pageIndex == self.MESSAGES_TAB:
810                                 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
811                         elif pageIndex == self.CONTACTS_TAB:
812                                 self._contactsViews[self._selectedBackendId].update()
813                         elif pageIndex == self.ACCOUNT_TAB:
814                                 self._accountViews[self._selectedBackendId].update()
815
816                         if didRecentUpdate or didMessagesUpdate:
817                                 if self._ledHandler is not None:
818                                         self._ledHandler.off()
819                 except Exception, e:
820                         self._errorDisplay.push_exception()
821
822         def _set_tab_refresh(self, *args):
823                 try:
824                         pageIndex = self._notebook.get_current_page()
825                         child = self._notebook.get_nth_page(pageIndex)
826                         self._notebook.get_tab_label(child).set_text("Refresh?")
827                 except Exception, e:
828                         self._errorDisplay.push_exception()
829                 return False
830
831         def _reset_tab_refresh(self, *args):
832                 try:
833                         pageIndex = self._notebook.get_current_page()
834                         child = self._notebook.get_nth_page(pageIndex)
835                         self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
836                 except Exception, e:
837                         self._errorDisplay.push_exception()
838                 return False
839
840         def _on_tab_refresh(self, *args):
841                 try:
842                         self._refresh_active_tab()
843                         self._reset_tab_refresh()
844                 except Exception, e:
845                         self._errorDisplay.push_exception()
846                 return False
847
848         def _on_sms_clicked(self, numbers, message):
849                 try:
850                         assert numbers, "No number specified"
851                         assert message, "Empty message"
852                         self.refresh_session()
853                         try:
854                                 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
855                         except Exception, e:
856                                 loggedIn = False
857                                 self._errorDisplay.push_exception()
858                                 return
859
860                         if not loggedIn:
861                                 self._errorDisplay.push_message(
862                                         "Backend link with GoogleVoice is not working, please try again"
863                                 )
864                                 return
865
866                         dialed = False
867                         try:
868                                 self._phoneBackends[self._selectedBackendId].send_sms(numbers, message)
869                                 hildonize.show_information_banner(self._window, "Sending to %s" % ", ".join(numbers))
870                                 _moduleLogger.info("Sending SMS to %r" % numbers)
871                                 dialed = True
872                         except Exception, e:
873                                 self._errorDisplay.push_exception()
874
875                         if dialed:
876                                 self._dialpads[self._selectedBackendId].clear()
877                 except Exception, e:
878                         self._errorDisplay.push_exception()
879
880         def _on_dial_clicked(self, number):
881                 try:
882                         assert number, "No number to call"
883                         self.refresh_session()
884                         try:
885                                 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
886                         except Exception, e:
887                                 loggedIn = False
888                                 self._errorDisplay.push_exception()
889                                 return
890
891                         if not loggedIn:
892                                 self._errorDisplay.push_message(
893                                         "Backend link with GoogleVoice is not working, please try again"
894                                 )
895                                 return
896
897                         dialed = False
898                         try:
899                                 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
900                                 self._phoneBackends[self._selectedBackendId].call(number)
901                                 hildonize.show_information_banner(self._window, "Calling %s" % number)
902                                 _moduleLogger.info("Calling %s" % number)
903                                 dialed = True
904                         except Exception, e:
905                                 self._errorDisplay.push_exception()
906
907                         if dialed:
908                                 self._dialpads[self._selectedBackendId].clear()
909                 except Exception, e:
910                         self._errorDisplay.push_exception()
911
912         def _import_contacts(self):
913                 csvFilter = gtk.FileFilter()
914                 csvFilter.set_name("Contacts")
915                 csvFilter.add_pattern("*.csv")
916                 importFileChooser = gtk.FileChooserDialog(
917                         title="Contacts",
918                         parent=self._window,
919                 )
920                 importFileChooser.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
921                 importFileChooser.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
922
923                 importFileChooser.set_property("filter", csvFilter)
924                 userResponse = importFileChooser.run()
925                 importFileChooser.hide()
926                 if userResponse == gtk.RESPONSE_OK:
927                         filename = importFileChooser.get_filename()
928                         shutil.copy2(filename, self._fsContactsPath)
929
930         def _on_menu_refresh(self, *args):
931                 try:
932                         self._refresh_active_tab()
933                 except Exception, e:
934                         self._errorDisplay.push_exception()
935
936         def _on_paste(self, *args):
937                 try:
938                         contents = self._clipboard.wait_for_text()
939                         if contents is not None:
940                                 self._dialpads[self._selectedBackendId].set_number(contents)
941                 except Exception, e:
942                         self._errorDisplay.push_exception()
943
944         def _on_about_activate(self, *args):
945                 try:
946                         dlg = gtk.AboutDialog()
947                         dlg.set_name(constants.__pretty_app_name__)
948                         dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
949                         dlg.set_copyright("Copyright 2008 - LGPL")
950                         dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account.  This application is not affiliated with Google in any way")
951                         dlg.set_website("http://gc-dialer.garage.maemo.org/")
952                         dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <eopage@byu.net>"])
953                         dlg.run()
954                         dlg.destroy()
955                 except Exception, e:
956                         self._errorDisplay.push_exception()
957
958
959 def run_doctest():
960         import doctest
961
962         failureCount, testCount = doctest.testmod()
963         if not failureCount:
964                 print "Tests Successful"
965                 sys.exit(0)
966         else:
967                 sys.exit(1)
968
969
970 def run_dialpad():
971         gtk.gdk.threads_init()
972
973         handle = Dialcentral()
974         if not PROFILE_STARTUP:
975                 gtk.main()
976
977
978 class DummyOptions(object):
979
980         def __init__(self):
981                 self.test = False
982
983
984 if __name__ == "__main__":
985         logging.basicConfig(level=logging.DEBUG)
986         try:
987                 if len(sys.argv) > 1:
988                         try:
989                                 import optparse
990                         except ImportError:
991                                 optparse = None
992
993                         if optparse is not None:
994                                 parser = optparse.OptionParser()
995                                 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
996                                 (commandOptions, commandArgs) = parser.parse_args()
997                 else:
998                         commandOptions = DummyOptions()
999                         commandArgs = []
1000
1001                 if commandOptions.test:
1002                         run_doctest()
1003                 else:
1004                         run_dialpad()
1005         finally:
1006                 logging.shutdown()