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